@hubspot/ui-extensions-dev-server 0.10.2 → 1.0.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.
Files changed (95) hide show
  1. package/README.md +23 -4
  2. package/dist/index.d.ts +3 -3
  3. package/dist/index.js +4 -45
  4. package/dist/lib/DevModeInterface.d.ts +2 -2
  5. package/dist/lib/DevModeInterface.js +12 -28
  6. package/dist/lib/DevModeParentInterface.d.ts +2 -2
  7. package/dist/lib/DevModeParentInterface.js +138 -154
  8. package/dist/lib/DevModeUnifiedInterface.d.ts +2 -2
  9. package/dist/lib/DevModeUnifiedInterface.js +28 -49
  10. package/dist/lib/DevServerState.d.ts +9 -5
  11. package/dist/lib/DevServerState.js +37 -18
  12. package/dist/lib/ExtensionsWebSocket.d.ts +25 -0
  13. package/dist/lib/ExtensionsWebSocket.js +110 -0
  14. package/dist/lib/__mocks__/config.d.ts +2 -0
  15. package/dist/lib/__mocks__/config.js +5 -0
  16. package/dist/lib/__mocks__/isExtensionFile.d.ts +5 -0
  17. package/dist/lib/__mocks__/isExtensionFile.js +11 -0
  18. package/dist/lib/__tests__/DevModeInterface.spec.d.ts +1 -0
  19. package/dist/lib/__tests__/DevModeInterface.spec.js +155 -0
  20. package/dist/lib/__tests__/DevModeParentInterface.spec.d.ts +1 -0
  21. package/dist/lib/__tests__/DevModeParentInterface.spec.js +179 -0
  22. package/dist/lib/__tests__/DevModeUnifiedInterface.spec.d.ts +1 -0
  23. package/dist/lib/__tests__/DevModeUnifiedInterface.spec.js +236 -0
  24. package/dist/lib/__tests__/ExtensionsWebSocket.spec.d.ts +1 -0
  25. package/dist/lib/__tests__/ExtensionsWebSocket.spec.js +304 -0
  26. package/dist/lib/__tests__/ast.spec.d.ts +1 -0
  27. package/dist/lib/__tests__/ast.spec.js +737 -0
  28. package/dist/lib/__tests__/build.spec.d.ts +1 -0
  29. package/dist/lib/__tests__/build.spec.js +159 -0
  30. package/dist/lib/__tests__/config.spec.d.ts +1 -0
  31. package/dist/lib/__tests__/config.spec.js +291 -0
  32. package/dist/lib/__tests__/dev.spec.d.ts +1 -0
  33. package/dist/lib/__tests__/dev.spec.js +80 -0
  34. package/dist/lib/__tests__/extensionsService.spec.d.ts +1 -0
  35. package/dist/lib/__tests__/extensionsService.spec.js +150 -0
  36. package/dist/lib/__tests__/factories.d.ts +48 -0
  37. package/dist/lib/__tests__/factories.js +32 -0
  38. package/dist/lib/__tests__/fixtures/extensionConfig.d.ts +182 -0
  39. package/dist/lib/__tests__/fixtures/extensionConfig.js +304 -0
  40. package/dist/lib/__tests__/fixtures/urls.d.ts +4 -0
  41. package/dist/lib/__tests__/fixtures/urls.js +4 -0
  42. package/dist/lib/__tests__/parsing-utils.spec.d.ts +1 -0
  43. package/dist/lib/__tests__/parsing-utils.spec.js +467 -0
  44. package/dist/lib/__tests__/plugins/codeBlockingPlugin.spec.d.ts +1 -0
  45. package/dist/lib/__tests__/plugins/codeBlockingPlugin.spec.js +112 -0
  46. package/dist/lib/__tests__/plugins/codeCheckingPlugin.spec.d.ts +1 -0
  47. package/dist/lib/__tests__/plugins/codeCheckingPlugin.spec.js +82 -0
  48. package/dist/lib/__tests__/plugins/devBuildPlugin.spec.d.ts +1 -0
  49. package/dist/lib/__tests__/plugins/devBuildPlugin.spec.js +256 -0
  50. package/dist/lib/__tests__/plugins/friendlyLoggingPlugin.spec.d.ts +1 -0
  51. package/dist/lib/__tests__/plugins/friendlyLoggingPlugin.spec.js +65 -0
  52. package/dist/lib/__tests__/plugins/manifestPlugin.spec.d.ts +1 -0
  53. package/dist/lib/__tests__/plugins/manifestPlugin.spec.js +455 -0
  54. package/dist/lib/__tests__/plugins/relevantModulesPlugin.spec.d.ts +1 -0
  55. package/dist/lib/__tests__/plugins/relevantModulesPlugin.spec.js +81 -0
  56. package/dist/lib/__tests__/server.spec.d.ts +1 -0
  57. package/dist/lib/__tests__/server.spec.js +152 -0
  58. package/dist/lib/__tests__/test-utils/ast.d.ts +1 -0
  59. package/dist/lib/__tests__/test-utils/ast.js +4 -0
  60. package/dist/lib/__tests__/utils.spec.d.ts +1 -0
  61. package/dist/lib/__tests__/utils.spec.js +176 -0
  62. package/dist/lib/ast.d.ts +1 -1
  63. package/dist/lib/ast.js +22 -29
  64. package/dist/lib/bin/cli.js +52 -72
  65. package/dist/lib/build.d.ts +1 -1
  66. package/dist/lib/build.js +60 -78
  67. package/dist/lib/config.d.ts +1 -1
  68. package/dist/lib/config.js +31 -34
  69. package/dist/lib/constants.d.ts +0 -2
  70. package/dist/lib/constants.js +20 -27
  71. package/dist/lib/dev.d.ts +1 -1
  72. package/dist/lib/dev.js +52 -69
  73. package/dist/lib/extensionsService.d.ts +1 -1
  74. package/dist/lib/extensionsService.js +21 -15
  75. package/dist/lib/parsing-utils.d.ts +1 -1
  76. package/dist/lib/parsing-utils.js +7 -11
  77. package/dist/lib/plugins/codeBlockingPlugin.d.ts +1 -1
  78. package/dist/lib/plugins/codeBlockingPlugin.js +5 -8
  79. package/dist/lib/plugins/codeCheckingPlugin.d.ts +1 -1
  80. package/dist/lib/plugins/codeCheckingPlugin.js +10 -11
  81. package/dist/lib/plugins/devBuildPlugin.d.ts +2 -2
  82. package/dist/lib/plugins/devBuildPlugin.js +74 -99
  83. package/dist/lib/plugins/friendlyLoggingPlugin.d.ts +2 -2
  84. package/dist/lib/plugins/friendlyLoggingPlugin.js +4 -12
  85. package/dist/lib/plugins/manifestPlugin.d.ts +1 -1
  86. package/dist/lib/plugins/manifestPlugin.js +46 -26
  87. package/dist/lib/plugins/relevantModulesPlugin.d.ts +2 -2
  88. package/dist/lib/plugins/relevantModulesPlugin.js +4 -7
  89. package/dist/lib/server.d.ts +7 -2
  90. package/dist/lib/server.js +85 -84
  91. package/dist/lib/types.d.ts +1 -1
  92. package/dist/lib/types.js +4 -7
  93. package/dist/lib/utils.d.ts +1 -1
  94. package/dist/lib/utils.js +23 -40
  95. package/package.json +44 -31
@@ -0,0 +1,304 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { ExtensionsWebSocket, isAllowedOrigin, } from "../ExtensionsWebSocket.js";
3
+ import { DevServerState } from "../DevServerState.js";
4
+ import { createMockLogger, createDevServerConfig } from "./factories.js";
5
+ const MOCK_INTERVAL_ID = 123;
6
+ let mockClients;
7
+ let mockWss;
8
+ let upgradeHandler = null;
9
+ vi.mock('ws', () => ({
10
+ WebSocketServer: vi.fn(() => {
11
+ mockClients = new Set();
12
+ const listeners = {};
13
+ let isClosed = false;
14
+ mockWss = {
15
+ clients: mockClients,
16
+ on: vi.fn((event, handler) => {
17
+ if (!listeners[event])
18
+ listeners[event] = [];
19
+ listeners[event].push(handler);
20
+ }),
21
+ emit: vi.fn((event, ...args) => {
22
+ listeners[event]?.forEach((handler) => handler(...args));
23
+ }),
24
+ handleUpgrade: vi.fn((request, socket, head, callback) => {
25
+ const mockClient = createMockWebSocket();
26
+ mockClients.add(mockClient);
27
+ callback(mockClient);
28
+ }),
29
+ close: vi.fn((callback) => {
30
+ if (isClosed) {
31
+ callback?.(new Error('The server is not running'));
32
+ }
33
+ else {
34
+ isClosed = true;
35
+ callback?.();
36
+ }
37
+ }),
38
+ };
39
+ return mockWss;
40
+ }),
41
+ WebSocket: { OPEN: 1, CLOSED: 3 },
42
+ }));
43
+ function createMockWebSocket() {
44
+ const listeners = {};
45
+ const mockWs = {
46
+ readyState: 1,
47
+ on: vi.fn((event, handler) => {
48
+ if (!listeners[event])
49
+ listeners[event] = [];
50
+ listeners[event].push(handler);
51
+ }),
52
+ once: vi.fn(),
53
+ emit: vi.fn((event, ...args) => {
54
+ listeners[event]?.forEach((handler) => handler(...args));
55
+ }),
56
+ send: vi.fn(),
57
+ close: vi.fn(() => {
58
+ mockWs.readyState = 3;
59
+ mockClients.delete(mockWs);
60
+ listeners['close']?.forEach((handler) => handler());
61
+ }),
62
+ terminate: vi.fn(() => {
63
+ mockWs.readyState = 3;
64
+ mockClients.delete(mockWs);
65
+ }),
66
+ ping: vi.fn(),
67
+ };
68
+ return mockWs;
69
+ }
70
+ let mockHttpServer;
71
+ let devServerState;
72
+ let extensionsWebSocket;
73
+ function createMockHttpServer() {
74
+ const listeners = {};
75
+ return {
76
+ on: vi.fn((event, handler) => {
77
+ if (!listeners[event])
78
+ listeners[event] = [];
79
+ listeners[event].push(handler);
80
+ if (event === 'upgrade') {
81
+ upgradeHandler = handler;
82
+ }
83
+ }),
84
+ emit: vi.fn((event, ...args) => {
85
+ listeners[event]?.forEach((handler) => handler(...args));
86
+ }),
87
+ listen: vi.fn(),
88
+ close: vi.fn(),
89
+ };
90
+ }
91
+ function simulateClientConnection(origin = 'http://localhost') {
92
+ if (!upgradeHandler)
93
+ throw new Error('upgradeHandler not initialized');
94
+ const mockRequest = { headers: { origin } };
95
+ const mockSocket = { destroy: vi.fn() };
96
+ upgradeHandler(mockRequest, mockSocket, Buffer.from([]));
97
+ return {
98
+ mockRequest,
99
+ mockSocket,
100
+ client: Array.from(mockClients)[mockClients.size - 1],
101
+ };
102
+ }
103
+ describe('ExtensionsWebSocket', () => {
104
+ describe('isAllowedOrigin', () => {
105
+ it('should return false for undefined origin', () => {
106
+ expect(isAllowedOrigin(undefined)).toBe(false);
107
+ });
108
+ it('should return true for localhost origins', () => {
109
+ expect(isAllowedOrigin('http://localhost')).toBe(true);
110
+ expect(isAllowedOrigin('https://localhost')).toBe(true);
111
+ expect(isAllowedOrigin('https://localhost:8080')).toBe(true);
112
+ expect(isAllowedOrigin('https://localhost:5173')).toBe(true);
113
+ });
114
+ it('should return true for hubspot.com origins', () => {
115
+ expect(isAllowedOrigin('https://app.hubspot.com')).toBe(true);
116
+ expect(isAllowedOrigin('http://hubspot.com')).toBe(true);
117
+ expect(isAllowedOrigin('https://api.hubspot.com:443')).toBe(true);
118
+ });
119
+ it('should return true for hubspotqa.com origins', () => {
120
+ expect(isAllowedOrigin('https://app.hubspotqa.com')).toBe(true);
121
+ expect(isAllowedOrigin('http://hubspotqa.com')).toBe(true);
122
+ expect(isAllowedOrigin('https://api.hubspotqa.com:443')).toBe(true);
123
+ });
124
+ it('should return false for non-allowed origins', () => {
125
+ expect(isAllowedOrigin('https://evil.com')).toBe(false);
126
+ expect(isAllowedOrigin('http://example.com')).toBe(false);
127
+ expect(isAllowedOrigin('https://hubspot.com.evil.com')).toBe(false);
128
+ });
129
+ });
130
+ describe('ExtensionsWebSocket class', () => {
131
+ beforeEach(() => {
132
+ const logger = createMockLogger();
133
+ const config = createDevServerConfig(logger);
134
+ devServerState = new DevServerState(config);
135
+ mockHttpServer = createMockHttpServer();
136
+ mockClients = new Set();
137
+ });
138
+ afterEach(() => {
139
+ vi.useRealTimers();
140
+ vi.clearAllMocks();
141
+ mockClients.clear();
142
+ });
143
+ describe('constructor', () => {
144
+ it('should create an ExtensionsWebSocket instance', () => {
145
+ extensionsWebSocket = new ExtensionsWebSocket(mockHttpServer, devServerState);
146
+ expect(extensionsWebSocket).toBeDefined();
147
+ });
148
+ });
149
+ describe('setupUpgradeHandler', () => {
150
+ it('should reject connections from non-allowed origins', () => {
151
+ extensionsWebSocket = new ExtensionsWebSocket(mockHttpServer, devServerState);
152
+ const mockSocket = {
153
+ destroy: vi.fn(),
154
+ };
155
+ const mockRequest = {
156
+ headers: {
157
+ origin: 'https://evil.com',
158
+ },
159
+ };
160
+ mockHttpServer.emit('upgrade', mockRequest, mockSocket, Buffer.from([]));
161
+ expect(mockSocket.destroy).toHaveBeenCalled();
162
+ expect(devServerState.logger.debug).toHaveBeenCalledWith('Rejected WebSocket: https://evil.com');
163
+ });
164
+ it('should accept connections from allowed origins', () => {
165
+ extensionsWebSocket = new ExtensionsWebSocket(mockHttpServer, devServerState);
166
+ simulateClientConnection('http://localhost');
167
+ expect(extensionsWebSocket.clientCount).toBe(1);
168
+ });
169
+ });
170
+ describe('broadcast', () => {
171
+ it('should log when no clients are connected', () => {
172
+ extensionsWebSocket = new ExtensionsWebSocket(mockHttpServer, devServerState);
173
+ extensionsWebSocket.broadcast({ event: 'test' });
174
+ expect(devServerState.logger.debug).toHaveBeenCalledWith('No clients connected, message not sent');
175
+ });
176
+ it('should send messages to all connected clients', () => {
177
+ extensionsWebSocket = new ExtensionsWebSocket(mockHttpServer, devServerState);
178
+ const { client: client1 } = simulateClientConnection();
179
+ const { client: client2 } = simulateClientConnection();
180
+ extensionsWebSocket.broadcast({ event: 'test', data: 'hello' });
181
+ expect(client1.send).toHaveBeenCalledWith(JSON.stringify({ event: 'test', data: 'hello' }));
182
+ expect(client2.send).toHaveBeenCalledWith(JSON.stringify({ event: 'test', data: 'hello' }));
183
+ });
184
+ it('should handle send failures gracefully', () => {
185
+ extensionsWebSocket = new ExtensionsWebSocket(mockHttpServer, devServerState);
186
+ const { client } = simulateClientConnection();
187
+ vi.mocked(client.send).mockImplementation(() => {
188
+ throw new Error('Send failed');
189
+ });
190
+ extensionsWebSocket.broadcast({ event: 'test' });
191
+ expect(devServerState.logger.warn).toHaveBeenCalledWith('Sent to 0 clients, 1 failed');
192
+ });
193
+ });
194
+ describe('onConnection', () => {
195
+ it('should register connection handler', () => {
196
+ extensionsWebSocket = new ExtensionsWebSocket(mockHttpServer, devServerState);
197
+ let connectionHandlerCalled = false;
198
+ extensionsWebSocket.onConnection(() => {
199
+ connectionHandlerCalled = true;
200
+ });
201
+ simulateClientConnection();
202
+ expect(connectionHandlerCalled).toBe(true);
203
+ });
204
+ });
205
+ describe('clientCount', () => {
206
+ it('should return 0 when no clients are connected', () => {
207
+ extensionsWebSocket = new ExtensionsWebSocket(mockHttpServer, devServerState);
208
+ expect(extensionsWebSocket.clientCount).toBe(0);
209
+ });
210
+ it('should return correct count of connected clients', () => {
211
+ extensionsWebSocket = new ExtensionsWebSocket(mockHttpServer, devServerState);
212
+ simulateClientConnection();
213
+ simulateClientConnection();
214
+ expect(extensionsWebSocket.clientCount).toBe(2);
215
+ });
216
+ });
217
+ describe('keepAlive', () => {
218
+ it('should terminate clients that do not respond to ping', () => {
219
+ let keepAliveCallback;
220
+ const setIntervalSpy = vi
221
+ .spyOn(global, 'setInterval')
222
+ .mockImplementation((fn) => {
223
+ keepAliveCallback = fn;
224
+ return MOCK_INTERVAL_ID;
225
+ });
226
+ extensionsWebSocket = new ExtensionsWebSocket(mockHttpServer, devServerState);
227
+ const { client } = simulateClientConnection();
228
+ // First keepAlive call: marks client as not alive and sends ping
229
+ keepAliveCallback();
230
+ // Second keepAlive call: should terminate since client didn't respond
231
+ keepAliveCallback();
232
+ expect(client.terminate).toHaveBeenCalled();
233
+ setIntervalSpy.mockRestore();
234
+ });
235
+ });
236
+ describe('client handlers', () => {
237
+ it('should log client errors', () => {
238
+ extensionsWebSocket = new ExtensionsWebSocket(mockHttpServer, devServerState);
239
+ const { client } = simulateClientConnection();
240
+ client.emit('error', new Error('Test error'));
241
+ expect(devServerState.logger.debug).toHaveBeenCalledWith('Client error: Test error');
242
+ });
243
+ it('should log client disconnections', () => {
244
+ extensionsWebSocket = new ExtensionsWebSocket(mockHttpServer, devServerState);
245
+ const { client } = simulateClientConnection();
246
+ client.close();
247
+ const debugCalls = vi.mocked(devServerState.logger.debug).mock.calls;
248
+ const hasDisconnectLog = debugCalls.some((call) => call[0].includes('Client disconnected'));
249
+ expect(hasDisconnectLog).toBe(true);
250
+ });
251
+ });
252
+ describe('close', () => {
253
+ it('should terminate all clients before closing', async () => {
254
+ extensionsWebSocket = new ExtensionsWebSocket(mockHttpServer, devServerState);
255
+ const { client: client1 } = simulateClientConnection();
256
+ const { client: client2 } = simulateClientConnection();
257
+ expect(extensionsWebSocket.clientCount).toBe(2);
258
+ await extensionsWebSocket.close();
259
+ expect(client1.terminate).toHaveBeenCalled();
260
+ expect(client2.terminate).toHaveBeenCalled();
261
+ expect(extensionsWebSocket.clientCount).toBe(0);
262
+ });
263
+ it('should close the WebSocket server after clients disconnect', async () => {
264
+ extensionsWebSocket = new ExtensionsWebSocket(mockHttpServer, devServerState);
265
+ const { client } = simulateClientConnection();
266
+ expect(extensionsWebSocket.clientCount).toBe(1);
267
+ client.close();
268
+ await extensionsWebSocket.close();
269
+ expect(extensionsWebSocket.clientCount).toBe(0);
270
+ });
271
+ it('should stop keep-alive mechanism after close', async () => {
272
+ extensionsWebSocket = new ExtensionsWebSocket(mockHttpServer, devServerState);
273
+ const { client } = simulateClientConnection();
274
+ const clearIntervalSpy = vi.spyOn(global, 'clearInterval');
275
+ client.close();
276
+ await extensionsWebSocket.close();
277
+ expect(clearIntervalSpy).toHaveBeenCalled();
278
+ clearIntervalSpy.mockRestore();
279
+ });
280
+ it('should reject when close is called multiple times', async () => {
281
+ extensionsWebSocket = new ExtensionsWebSocket(mockHttpServer, devServerState);
282
+ await extensionsWebSocket.close();
283
+ await expect(extensionsWebSocket.close()).rejects.toThrow('The server is not running');
284
+ });
285
+ it('should not broadcast after close', async () => {
286
+ extensionsWebSocket = new ExtensionsWebSocket(mockHttpServer, devServerState);
287
+ const { client } = simulateClientConnection();
288
+ client.close();
289
+ await extensionsWebSocket.close();
290
+ extensionsWebSocket.broadcast({ event: 'test' });
291
+ expect(devServerState.logger.debug).toHaveBeenCalledWith('No clients connected, message not sent');
292
+ });
293
+ });
294
+ describe('error handlers', () => {
295
+ it('should log WebSocket server errors', () => {
296
+ extensionsWebSocket = new ExtensionsWebSocket(mockHttpServer, devServerState);
297
+ const testError = new Error('Server error');
298
+ const errorHandler = vi.mocked(mockWss.on).mock.calls.find((call) => call[0] === 'error')?.[1];
299
+ errorHandler?.(testError);
300
+ expect(devServerState.logger.error).toHaveBeenCalledWith('WebSocket server error: Error: Server error');
301
+ });
302
+ });
303
+ });
304
+ });
@@ -0,0 +1 @@
1
+ export {};