@hubspot/cli 8.0.1-experimental.0 → 8.0.3-experimental.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/bin/cli.js +8 -5
- package/commands/__tests__/getStarted.test.js +12 -0
- package/commands/__tests__/project.test.js +30 -0
- package/commands/account/auth.js +8 -97
- package/commands/account/use.js +19 -4
- package/commands/cms/module/marketplace-validate.js +23 -5
- package/commands/cms/theme/marketplace-validate.js +25 -6
- package/commands/getStarted.d.ts +2 -1
- package/commands/getStarted.js +38 -15
- package/commands/mcp/__tests__/start.test.js +1 -67
- package/commands/mcp/setup.js +1 -2
- package/commands/mcp/start.js +1 -19
- package/commands/mcp.js +1 -2
- package/commands/project.js +22 -1
- package/lang/en.d.ts +53 -7
- package/lang/en.js +59 -13
- package/lib/CLIWebSocketServer.d.ts +28 -0
- package/lib/CLIWebSocketServer.js +91 -0
- package/lib/__tests__/CLIWebSocketServer.test.d.ts +1 -0
- package/lib/__tests__/CLIWebSocketServer.test.js +252 -0
- package/lib/__tests__/accountAuth.test.d.ts +1 -0
- package/lib/__tests__/accountAuth.test.js +258 -0
- package/lib/__tests__/commandSuggestion.test.d.ts +1 -0
- package/lib/__tests__/commandSuggestion.test.js +119 -0
- package/lib/accountAuth.d.ts +10 -0
- package/lib/accountAuth.js +105 -0
- package/lib/app/urls.d.ts +1 -0
- package/lib/app/urls.js +4 -0
- package/lib/commandSuggestion.d.ts +3 -0
- package/lib/commandSuggestion.js +45 -0
- package/lib/constants.d.ts +0 -1
- package/lib/constants.js +0 -1
- package/lib/errors/ProjectErrors.d.ts +15 -0
- package/lib/errors/ProjectErrors.js +30 -0
- package/lib/getStarted/getStartedV2.d.ts +7 -0
- package/lib/getStarted/getStartedV2.js +18 -0
- package/lib/getStartedV2Actions.d.ts +37 -0
- package/lib/getStartedV2Actions.js +146 -0
- package/lib/marketplaceValidate.d.ts +1 -1
- package/lib/marketplaceValidate.js +23 -41
- package/lib/mcp/__tests__/setup.test.js +0 -17
- package/lib/mcp/setup.d.ts +0 -1
- package/lib/mcp/setup.js +59 -103
- package/lib/projects/ProjectLogsManager.d.ts +12 -3
- package/lib/projects/ProjectLogsManager.js +70 -12
- package/lib/projects/__tests__/LocalDevWebsocketServer.test.js +43 -175
- package/lib/projects/__tests__/ProjectLogsManager.test.js +131 -18
- package/lib/projects/__tests__/platformVersion.test.js +37 -1
- package/lib/projects/__tests__/projects.test.js +6 -2
- package/lib/projects/components.d.ts +6 -0
- package/lib/projects/components.js +1 -1
- package/lib/projects/config.js +9 -2
- package/lib/projects/localDev/LocalDevWebsocketServer.d.ts +2 -7
- package/lib/projects/localDev/LocalDevWebsocketServer.js +51 -98
- package/lib/projects/localDev/helpers/project.d.ts +4 -1
- package/lib/projects/localDev/helpers/project.js +13 -8
- package/lib/projects/localDev/localDevWebsocketServerUtils.d.ts +8 -7
- package/lib/projects/platformVersion.d.ts +8 -0
- package/lib/projects/platformVersion.js +31 -2
- package/lib/prompts/accountsPrompt.d.ts +2 -1
- package/lib/prompts/accountsPrompt.js +10 -2
- package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +20 -3
- package/mcp-server/tools/project/AddFeatureToProjectTool.js +6 -10
- package/mcp-server/tools/project/CreateProjectTool.d.ts +24 -4
- package/mcp-server/tools/project/CreateProjectTool.js +5 -10
- package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.js +5 -8
- package/mcp-server/tools/project/GetBuildLogsTool.d.ts +2 -2
- package/mcp-server/tools/project/GetBuildLogsTool.js +3 -4
- package/mcp-server/tools/project/GetBuildStatusTool.d.ts +1 -1
- package/mcp-server/tools/project/GetBuildStatusTool.js +3 -4
- package/mcp-server/tools/project/GuidedWalkthroughTool.d.ts +6 -1
- package/mcp-server/tools/project/GuidedWalkthroughTool.js +1 -6
- package/mcp-server/tools/project/constants.d.ts +12 -1
- package/mcp-server/tools/project/constants.js +12 -16
- package/mcp-server/utils/__tests__/project.test.js +0 -125
- package/mcp-server/utils/project.js +0 -8
- package/package.json +10 -5
- package/types/LocalDev.d.ts +0 -4
- package/ui/components/ActionSection.d.ts +12 -0
- package/ui/components/ActionSection.js +25 -0
- package/ui/components/BoxWithTitle.d.ts +4 -2
- package/ui/components/BoxWithTitle.js +2 -2
- package/ui/components/FullScreen.d.ts +6 -0
- package/ui/components/FullScreen.js +13 -0
- package/ui/components/InputField.d.ts +10 -0
- package/ui/components/InputField.js +10 -0
- package/ui/components/SelectInput.d.ts +11 -0
- package/ui/components/SelectInput.js +59 -0
- package/ui/components/StatusIcon.d.ts +9 -0
- package/ui/components/StatusIcon.js +17 -0
- package/ui/components/getStarted/GetStartedFlow.d.ts +8 -0
- package/ui/components/getStarted/GetStartedFlow.js +136 -0
- package/ui/components/getStarted/reducer.d.ts +59 -0
- package/ui/components/getStarted/reducer.js +72 -0
- package/ui/components/getStarted/screens/ProjectSetupScreen.d.ts +16 -0
- package/ui/components/getStarted/screens/ProjectSetupScreen.js +39 -0
- package/ui/components/getStarted/screens/UploadScreen.d.ts +7 -0
- package/ui/components/getStarted/screens/UploadScreen.js +43 -0
- package/ui/components/getStarted/selectors.d.ts +2 -0
- package/ui/components/getStarted/selectors.js +1 -0
- package/ui/constants.d.ts +6 -0
- package/ui/constants.js +6 -0
- package/ui/lib/constants.d.ts +16 -0
- package/ui/lib/constants.js +16 -0
- package/ui/playground/fixtures.js +47 -0
- package/ui/render.d.ts +4 -0
- package/ui/render.js +31 -0
- package/ui/styles.d.ts +3 -0
- package/ui/styles.js +3 -0
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { WebSocketServer } from 'ws';
|
|
2
2
|
import { isPortManagerServerRunning, requestPorts, } from '@hubspot/local-dev-lib/portManager';
|
|
3
|
-
import { uiLogger } from '../../ui/logger.js';
|
|
4
3
|
import { LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES, LOCAL_DEV_UI_MESSAGE_SEND_TYPES, LOCAL_DEV_SERVER_MESSAGE_TYPES, } from '../../constants.js';
|
|
5
4
|
import LocalDevWebsocketServer from '../localDev/LocalDevWebsocketServer.js';
|
|
6
|
-
import { lib } from '../../../lang/en.js';
|
|
7
5
|
vi.mock('ws');
|
|
8
6
|
vi.mock('@hubspot/local-dev-lib/portManager');
|
|
9
7
|
describe('LocalDevWebsocketServer', () => {
|
|
@@ -12,19 +10,15 @@ describe('LocalDevWebsocketServer', () => {
|
|
|
12
10
|
let mockWebSocketServer;
|
|
13
11
|
let server;
|
|
14
12
|
beforeEach(() => {
|
|
15
|
-
// Reset all mocks
|
|
16
|
-
// Setup mock WebSocket
|
|
17
13
|
mockWebSocket = {
|
|
18
14
|
on: vi.fn(),
|
|
19
15
|
send: vi.fn(),
|
|
20
16
|
close: vi.fn(),
|
|
21
17
|
};
|
|
22
|
-
// Setup mock WebSocketServer
|
|
23
18
|
mockWebSocketServer = {
|
|
24
19
|
on: vi.fn(),
|
|
25
20
|
close: vi.fn(),
|
|
26
21
|
};
|
|
27
|
-
// Setup mock LocalDevProcess
|
|
28
22
|
mockLocalDevProcess = {
|
|
29
23
|
addStateListener: vi.fn(),
|
|
30
24
|
removeStateListener: vi.fn(),
|
|
@@ -39,116 +33,47 @@ describe('LocalDevWebsocketServer', () => {
|
|
|
39
33
|
targetProjectAccountId: 456,
|
|
40
34
|
targetTestingAccountId: 789,
|
|
41
35
|
};
|
|
42
|
-
// Mock WebSocketServer constructor
|
|
43
36
|
WebSocketServer.mockImplementation(() => mockWebSocketServer);
|
|
44
|
-
// Create server instance
|
|
45
37
|
server = new LocalDevWebsocketServer(mockLocalDevProcess, true);
|
|
46
38
|
});
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
39
|
+
function startServerAndConnect(ws) {
|
|
40
|
+
const connectionCallback = mockWebSocketServer.on.mock.calls[0][1];
|
|
41
|
+
connectionCallback(ws ?? mockWebSocket, {
|
|
42
|
+
headers: { origin: 'https://app.hubspot.com' },
|
|
51
43
|
});
|
|
52
|
-
|
|
44
|
+
}
|
|
45
|
+
describe('start()', () => {
|
|
46
|
+
beforeEach(async () => {
|
|
53
47
|
isPortManagerServerRunning.mockResolvedValue(true);
|
|
54
48
|
requestPorts.mockResolvedValue({
|
|
55
49
|
'local-dev-ui-websocket-server': 1234,
|
|
56
50
|
});
|
|
57
51
|
await server.start();
|
|
58
|
-
expect(WebSocketServer).toHaveBeenCalledWith({ port: 1234 });
|
|
59
|
-
expect(mockWebSocketServer.on).toHaveBeenCalledWith('connection', expect.any(Function));
|
|
60
|
-
expect(uiLogger.log).toHaveBeenCalled();
|
|
61
52
|
});
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
'https://app.hubspotqa.com',
|
|
66
|
-
'https://local.hubspot.com',
|
|
67
|
-
'https://local.hubspotqa.com',
|
|
68
|
-
'https://app-na2.hubspot.com',
|
|
69
|
-
'https://app-na2.hubspotqa.com',
|
|
70
|
-
'https://app-na3.hubspot.com',
|
|
71
|
-
'https://app-na3.hubspotqa.com',
|
|
72
|
-
'https://app-ap1.hubspot.com',
|
|
73
|
-
'https://app-ap1.hubspotqa.com',
|
|
74
|
-
'https://app-eu1.hubspot.com',
|
|
75
|
-
'https://app-eu1.hubspotqa.com',
|
|
76
|
-
];
|
|
77
|
-
validOrigins.forEach(origin => {
|
|
78
|
-
it(`should accept connection from ${origin}`, async () => {
|
|
79
|
-
isPortManagerServerRunning.mockResolvedValue(true);
|
|
80
|
-
requestPorts.mockResolvedValue({
|
|
81
|
-
'local-dev-ui-websocket-server': 1234,
|
|
82
|
-
});
|
|
83
|
-
await server.start();
|
|
84
|
-
// Get the connection callback
|
|
85
|
-
const connectionCallback = mockWebSocketServer.on.mock
|
|
86
|
-
.calls[0][1];
|
|
87
|
-
// Simulate connection from valid origin
|
|
88
|
-
connectionCallback(mockWebSocket, {
|
|
89
|
-
headers: { origin },
|
|
90
|
-
});
|
|
91
|
-
expect(mockWebSocket.on).toHaveBeenCalledWith('message', expect.any(Function));
|
|
92
|
-
expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledWith('projectNodes', expect.any(Function));
|
|
93
|
-
expect(mockWebSocket.close).not.toHaveBeenCalled();
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
describe('invalid origins', () => {
|
|
98
|
-
const invalidOrigins = [
|
|
99
|
-
'https://malicious-site.com',
|
|
100
|
-
'https://app.malicious-site.com',
|
|
101
|
-
'https://app.hubspot.com.evil.com',
|
|
102
|
-
];
|
|
103
|
-
invalidOrigins.forEach(origin => {
|
|
104
|
-
it(`should reject connection from "${origin}"`, async () => {
|
|
105
|
-
isPortManagerServerRunning.mockResolvedValue(true);
|
|
106
|
-
requestPorts.mockResolvedValue({
|
|
107
|
-
'local-dev-ui-websocket-server': 1234,
|
|
108
|
-
});
|
|
109
|
-
await server.start();
|
|
110
|
-
// Get the connection callback
|
|
111
|
-
const connectionCallback = mockWebSocketServer.on.mock
|
|
112
|
-
.calls[0][1];
|
|
113
|
-
// Simulate connection from invalid origin
|
|
114
|
-
connectionCallback(mockWebSocket, {
|
|
115
|
-
headers: { origin },
|
|
116
|
-
});
|
|
117
|
-
expect(mockWebSocket.close).toHaveBeenCalledWith(1008, lib.LocalDevWebsocketServer.errors.originNotAllowed(origin));
|
|
118
|
-
expect(mockWebSocket.on).not.toHaveBeenCalled();
|
|
119
|
-
expect(mockLocalDevProcess.addStateListener).not.toHaveBeenCalled();
|
|
120
|
-
});
|
|
121
|
-
});
|
|
53
|
+
it('should send WEBSOCKET_SERVER_CONNECTED message when valid connection is established', () => {
|
|
54
|
+
startServerAndConnect();
|
|
55
|
+
expect(mockLocalDevProcess.sendDevServerMessage).toHaveBeenCalledWith(LOCAL_DEV_SERVER_MESSAGE_TYPES.WEBSOCKET_SERVER_CONNECTED);
|
|
122
56
|
});
|
|
123
|
-
it('should
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
expect(mockWebSocket.on).not.toHaveBeenCalled();
|
|
137
|
-
expect(mockLocalDevProcess.addStateListener).not.toHaveBeenCalled();
|
|
57
|
+
it('should send project data on connection', () => {
|
|
58
|
+
startServerAndConnect();
|
|
59
|
+
expect(mockWebSocket.send).toHaveBeenCalledWith(JSON.stringify({
|
|
60
|
+
type: LOCAL_DEV_UI_MESSAGE_SEND_TYPES.UPDATE_PROJECT_DATA,
|
|
61
|
+
data: {
|
|
62
|
+
projectName: 'test-project',
|
|
63
|
+
projectId: 123,
|
|
64
|
+
latestBuild: { id: 'build-1', status: 'SUCCESS' },
|
|
65
|
+
deployedBuild: { id: 'build-1', status: 'SUCCESS' },
|
|
66
|
+
targetProjectAccountId: 456,
|
|
67
|
+
targetTestingAccountId: 789,
|
|
68
|
+
},
|
|
69
|
+
}));
|
|
138
70
|
});
|
|
139
|
-
it('should
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// Get the connection callback
|
|
146
|
-
const connectionCallback = mockWebSocketServer.on.mock.calls[0][1];
|
|
147
|
-
// Simulate connection from valid origin
|
|
148
|
-
connectionCallback(mockWebSocket, {
|
|
149
|
-
headers: { origin: 'https://app-na3.hubspot.com' },
|
|
150
|
-
});
|
|
151
|
-
expect(mockLocalDevProcess.sendDevServerMessage).toHaveBeenCalledWith(LOCAL_DEV_SERVER_MESSAGE_TYPES.WEBSOCKET_SERVER_CONNECTED);
|
|
71
|
+
it('should setup state listeners on connection', () => {
|
|
72
|
+
startServerAndConnect();
|
|
73
|
+
expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledWith('projectNodes', expect.any(Function));
|
|
74
|
+
expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledWith('appData', expect.any(Function));
|
|
75
|
+
expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledWith('uploadWarnings', expect.any(Function));
|
|
76
|
+
expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledWith('devServersStarted', expect.any(Function));
|
|
152
77
|
});
|
|
153
78
|
});
|
|
154
79
|
describe('message handling', () => {
|
|
@@ -158,39 +83,13 @@ describe('LocalDevWebsocketServer', () => {
|
|
|
158
83
|
'local-dev-ui-websocket-server': 1234,
|
|
159
84
|
});
|
|
160
85
|
await server.start();
|
|
161
|
-
|
|
162
|
-
connectionCallback(mockWebSocket, {
|
|
163
|
-
headers: { origin: 'https://app.hubspot.com' },
|
|
164
|
-
});
|
|
86
|
+
startServerAndConnect();
|
|
165
87
|
});
|
|
166
88
|
it('should handle UPLOAD message type', () => {
|
|
167
|
-
const messageCallback = mockWebSocket.on.mock.calls[0][1];
|
|
168
|
-
|
|
169
|
-
type: LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES.UPLOAD,
|
|
170
|
-
};
|
|
171
|
-
messageCallback(JSON.stringify(message));
|
|
89
|
+
const messageCallback = mockWebSocket.on.mock.calls.find(call => call[0] === 'message')[1];
|
|
90
|
+
messageCallback(JSON.stringify({ type: LOCAL_DEV_UI_MESSAGE_RECEIVE_TYPES.UPLOAD }));
|
|
172
91
|
expect(mockLocalDevProcess.uploadProject).toHaveBeenCalled();
|
|
173
92
|
});
|
|
174
|
-
it('should log error for missing message type', () => {
|
|
175
|
-
const messageCallback = mockWebSocket.on.mock.calls[0][1];
|
|
176
|
-
const message = {};
|
|
177
|
-
messageCallback(JSON.stringify(message));
|
|
178
|
-
expect(uiLogger.error).toHaveBeenCalled();
|
|
179
|
-
});
|
|
180
|
-
it('should log error for unknown message type', () => {
|
|
181
|
-
const messageCallback = mockWebSocket.on.mock.calls[0][1];
|
|
182
|
-
const message = {
|
|
183
|
-
type: 'UNKNOWN_TYPE',
|
|
184
|
-
};
|
|
185
|
-
messageCallback(JSON.stringify(message));
|
|
186
|
-
expect(uiLogger.error).toHaveBeenCalled();
|
|
187
|
-
});
|
|
188
|
-
it('should log error for invalid JSON', () => {
|
|
189
|
-
const messageCallback = mockWebSocket.on.mock.calls[0][1];
|
|
190
|
-
const invalidJson = 'invalid json';
|
|
191
|
-
messageCallback(invalidJson);
|
|
192
|
-
expect(uiLogger.error).toHaveBeenCalled();
|
|
193
|
-
});
|
|
194
93
|
});
|
|
195
94
|
describe('shutdown()', () => {
|
|
196
95
|
it('should close the websocket server', async () => {
|
|
@@ -209,7 +108,6 @@ describe('LocalDevWebsocketServer', () => {
|
|
|
209
108
|
let mockWebSocket3;
|
|
210
109
|
let connectionCallback;
|
|
211
110
|
beforeEach(async () => {
|
|
212
|
-
// Setup multiple mock WebSockets
|
|
213
111
|
mockWebSocket1 = {
|
|
214
112
|
on: vi.fn(),
|
|
215
113
|
send: vi.fn(),
|
|
@@ -225,17 +123,14 @@ describe('LocalDevWebsocketServer', () => {
|
|
|
225
123
|
send: vi.fn(),
|
|
226
124
|
close: vi.fn(),
|
|
227
125
|
};
|
|
228
|
-
// Start the server
|
|
229
126
|
isPortManagerServerRunning.mockResolvedValue(true);
|
|
230
127
|
requestPorts.mockResolvedValue({
|
|
231
128
|
'local-dev-ui-websocket-server': 1234,
|
|
232
129
|
});
|
|
233
130
|
await server.start();
|
|
234
|
-
// Get the connection callback
|
|
235
131
|
connectionCallback = mockWebSocketServer.on.mock.calls[0][1];
|
|
236
132
|
});
|
|
237
133
|
it('should handle multiple valid connections simultaneously', () => {
|
|
238
|
-
// Establish three connections from valid origins
|
|
239
134
|
connectionCallback(mockWebSocket1, {
|
|
240
135
|
headers: { origin: 'https://app.hubspot.com' },
|
|
241
136
|
});
|
|
@@ -245,30 +140,24 @@ describe('LocalDevWebsocketServer', () => {
|
|
|
245
140
|
connectionCallback(mockWebSocket3, {
|
|
246
141
|
headers: { origin: 'https://local.hubspot.com' },
|
|
247
142
|
});
|
|
248
|
-
// All connections should be established with proper setup
|
|
249
143
|
expect(mockWebSocket1.on).toHaveBeenCalledWith('message', expect.any(Function));
|
|
250
144
|
expect(mockWebSocket2.on).toHaveBeenCalledWith('message', expect.any(Function));
|
|
251
145
|
expect(mockWebSocket3.on).toHaveBeenCalledWith('message', expect.any(Function));
|
|
252
|
-
|
|
253
|
-
expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledTimes(12); // 4 listeners per connection * 3 connections
|
|
254
|
-
// Each connection should trigger dev server message
|
|
146
|
+
expect(mockLocalDevProcess.addStateListener).toHaveBeenCalledTimes(12);
|
|
255
147
|
expect(mockLocalDevProcess.sendDevServerMessage).toHaveBeenCalledTimes(3);
|
|
256
148
|
expect(mockLocalDevProcess.sendDevServerMessage).toHaveBeenCalledWith(LOCAL_DEV_SERVER_MESSAGE_TYPES.WEBSOCKET_SERVER_CONNECTED);
|
|
257
|
-
// No connections should be closed
|
|
258
149
|
expect(mockWebSocket1.close).not.toHaveBeenCalled();
|
|
259
150
|
expect(mockWebSocket2.close).not.toHaveBeenCalled();
|
|
260
151
|
expect(mockWebSocket3.close).not.toHaveBeenCalled();
|
|
261
152
|
});
|
|
262
153
|
it('should send project data to each connection independently', () => {
|
|
263
|
-
// Establish multiple connections
|
|
264
154
|
connectionCallback(mockWebSocket1, {
|
|
265
155
|
headers: { origin: 'https://app.hubspot.com' },
|
|
266
156
|
});
|
|
267
157
|
connectionCallback(mockWebSocket2, {
|
|
268
158
|
headers: { origin: 'https://app-eu1.hubspotqa.com' },
|
|
269
159
|
});
|
|
270
|
-
|
|
271
|
-
expect(mockWebSocket1.send).toHaveBeenCalledWith(JSON.stringify({
|
|
160
|
+
const expectedProjectData = JSON.stringify({
|
|
272
161
|
type: LOCAL_DEV_UI_MESSAGE_SEND_TYPES.UPDATE_PROJECT_DATA,
|
|
273
162
|
data: {
|
|
274
163
|
projectName: 'test-project',
|
|
@@ -278,59 +167,41 @@ describe('LocalDevWebsocketServer', () => {
|
|
|
278
167
|
targetProjectAccountId: 456,
|
|
279
168
|
targetTestingAccountId: 789,
|
|
280
169
|
},
|
|
281
|
-
})
|
|
282
|
-
expect(
|
|
283
|
-
|
|
284
|
-
data: {
|
|
285
|
-
projectName: 'test-project',
|
|
286
|
-
projectId: 123,
|
|
287
|
-
latestBuild: { id: 'build-1', status: 'SUCCESS' },
|
|
288
|
-
deployedBuild: { id: 'build-1', status: 'SUCCESS' },
|
|
289
|
-
targetProjectAccountId: 456,
|
|
290
|
-
targetTestingAccountId: 789,
|
|
291
|
-
},
|
|
292
|
-
}));
|
|
170
|
+
});
|
|
171
|
+
expect(mockWebSocket1.send).toHaveBeenCalledWith(expectedProjectData);
|
|
172
|
+
expect(mockWebSocket2.send).toHaveBeenCalledWith(expectedProjectData);
|
|
293
173
|
});
|
|
294
174
|
it('should properly cleanup listeners when connections close', () => {
|
|
295
|
-
// Establish connections
|
|
296
175
|
connectionCallback(mockWebSocket1, {
|
|
297
176
|
headers: { origin: 'https://app.hubspot.com' },
|
|
298
177
|
});
|
|
299
178
|
connectionCallback(mockWebSocket2, {
|
|
300
179
|
headers: { origin: 'https://app-ap1.hubspotqa.com' },
|
|
301
180
|
});
|
|
302
|
-
// Get all the close callbacks for both connections (there should be 2 per connection)
|
|
303
181
|
const closeCallbacks1 = mockWebSocket1.on.mock.calls
|
|
304
182
|
.filter(call => call[0] === 'close')
|
|
305
183
|
.map(call => call[1]);
|
|
306
184
|
const closeCallbacks2 = mockWebSocket2.on.mock.calls
|
|
307
185
|
.filter(call => call[0] === 'close')
|
|
308
186
|
.map(call => call[1]);
|
|
309
|
-
expect(closeCallbacks1).toHaveLength(4);
|
|
310
|
-
expect(closeCallbacks2).toHaveLength(4);
|
|
311
|
-
// Simulate first connection closing (call all close callbacks)
|
|
187
|
+
expect(closeCallbacks1).toHaveLength(4);
|
|
188
|
+
expect(closeCallbacks2).toHaveLength(4);
|
|
312
189
|
closeCallbacks1.forEach(callback => callback());
|
|
313
|
-
// Should have removed listeners for first connection (4 listeners: projectNodes, appData, devServersStarted, and uploadWarnings)
|
|
314
190
|
expect(mockLocalDevProcess.removeStateListener).toHaveBeenCalledTimes(4);
|
|
315
|
-
// Simulate second connection closing
|
|
316
191
|
closeCallbacks2.forEach(callback => callback());
|
|
317
|
-
// Should have removed listeners for second connection as well
|
|
318
192
|
expect(mockLocalDevProcess.removeStateListener).toHaveBeenCalledTimes(8);
|
|
319
193
|
});
|
|
320
194
|
it('should broadcast state changes to all connected clients', () => {
|
|
321
|
-
// Establish connections
|
|
322
195
|
connectionCallback(mockWebSocket1, {
|
|
323
196
|
headers: { origin: 'https://app.hubspot.com' },
|
|
324
197
|
});
|
|
325
198
|
connectionCallback(mockWebSocket2, {
|
|
326
199
|
headers: { origin: 'https://local.hubspotqa.com' },
|
|
327
200
|
});
|
|
328
|
-
// Get the projectNodes listeners that were registered
|
|
329
201
|
const projectNodesListeners = mockLocalDevProcess.addStateListener.mock.calls
|
|
330
202
|
.filter(call => call[0] === 'projectNodes')
|
|
331
203
|
.map(call => call[1]);
|
|
332
204
|
expect(projectNodesListeners).toHaveLength(2);
|
|
333
|
-
// Simulate a project nodes update by calling the listeners
|
|
334
205
|
const mockProjectNodes = {
|
|
335
206
|
component1: {
|
|
336
207
|
uid: 'component1',
|
|
@@ -349,15 +220,12 @@ describe('LocalDevWebsocketServer', () => {
|
|
|
349
220
|
},
|
|
350
221
|
};
|
|
351
222
|
projectNodesListeners.forEach(listener => listener(mockProjectNodes));
|
|
352
|
-
|
|
353
|
-
expect(mockWebSocket1.send).toHaveBeenCalledWith(JSON.stringify({
|
|
354
|
-
type: LOCAL_DEV_UI_MESSAGE_SEND_TYPES.UPDATE_PROJECT_NODES,
|
|
355
|
-
data: mockProjectNodes,
|
|
356
|
-
}));
|
|
357
|
-
expect(mockWebSocket2.send).toHaveBeenCalledWith(JSON.stringify({
|
|
223
|
+
const expectedMessage = JSON.stringify({
|
|
358
224
|
type: LOCAL_DEV_UI_MESSAGE_SEND_TYPES.UPDATE_PROJECT_NODES,
|
|
359
225
|
data: mockProjectNodes,
|
|
360
|
-
})
|
|
226
|
+
});
|
|
227
|
+
expect(mockWebSocket1.send).toHaveBeenCalledWith(expectedMessage);
|
|
228
|
+
expect(mockWebSocket2.send).toHaveBeenCalledWith(expectedMessage);
|
|
361
229
|
});
|
|
362
230
|
});
|
|
363
231
|
});
|
|
@@ -2,6 +2,9 @@ import { ProjectLogsManager } from '../ProjectLogsManager.js';
|
|
|
2
2
|
import { getProjectConfig } from '../config.js';
|
|
3
3
|
import { ensureProjectExists } from '../ensureProjectExists.js';
|
|
4
4
|
import { fetchProjectComponentsMetadata } from '@hubspot/local-dev-lib/api/projects';
|
|
5
|
+
import { fetchAppMetadataBySourceId } from '@hubspot/local-dev-lib/api/appsDev';
|
|
6
|
+
import { getDeployedProjectNodes } from '../localDev/helpers/project.js';
|
|
7
|
+
import { isV2Project } from '../platformVersion.js';
|
|
5
8
|
const SUBCOMPONENT_TYPES = {
|
|
6
9
|
APP_ID: 'APP_ID',
|
|
7
10
|
PACKAGE_LOCK_FILE: 'PACKAGE_LOCK_FILE',
|
|
@@ -17,11 +20,20 @@ const SUBCOMPONENT_TYPES = {
|
|
|
17
20
|
vi.mock('../../projects/config');
|
|
18
21
|
vi.mock('../../projects/ensureProjectExists');
|
|
19
22
|
vi.mock('@hubspot/local-dev-lib/api/projects');
|
|
23
|
+
vi.mock('@hubspot/local-dev-lib/api/appsDev');
|
|
24
|
+
vi.mock('../../projects/localDev/helpers/project');
|
|
25
|
+
vi.mock('../../projects/platformVersion');
|
|
20
26
|
describe('lib/projects/ProjectLogsManager', () => {
|
|
21
27
|
const accountId = 12345678;
|
|
22
28
|
const appId = 999999;
|
|
23
29
|
const projectName = 'super cool test project';
|
|
24
|
-
const projectConfig = {
|
|
30
|
+
const projectConfig = {
|
|
31
|
+
projectConfig: {
|
|
32
|
+
name: projectName,
|
|
33
|
+
srcDir: 'src',
|
|
34
|
+
platformVersion: '2024.1',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
25
37
|
const projectId = 987654321;
|
|
26
38
|
const projectDetails = {
|
|
27
39
|
project: {
|
|
@@ -33,16 +45,26 @@ describe('lib/projects/ProjectLogsManager', () => {
|
|
|
33
45
|
};
|
|
34
46
|
const function1 = {
|
|
35
47
|
componentName: 'function1',
|
|
36
|
-
|
|
37
|
-
name: SUBCOMPONENT_TYPES.APP_FUNCTION,
|
|
38
|
-
},
|
|
39
|
-
deployOutput: {
|
|
40
|
-
appId,
|
|
41
|
-
appFunctionName: 'function1',
|
|
42
|
-
},
|
|
48
|
+
appId,
|
|
43
49
|
};
|
|
44
50
|
const functions = [
|
|
45
51
|
function1,
|
|
52
|
+
{
|
|
53
|
+
componentName: 'function2',
|
|
54
|
+
appId,
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
const legacyApiFunctions = [
|
|
58
|
+
{
|
|
59
|
+
componentName: 'function1',
|
|
60
|
+
type: {
|
|
61
|
+
name: SUBCOMPONENT_TYPES.APP_FUNCTION,
|
|
62
|
+
},
|
|
63
|
+
deployOutput: {
|
|
64
|
+
appId,
|
|
65
|
+
appFunctionName: 'function1',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
46
68
|
{
|
|
47
69
|
componentName: 'function2',
|
|
48
70
|
type: {
|
|
@@ -56,6 +78,7 @@ describe('lib/projects/ProjectLogsManager', () => {
|
|
|
56
78
|
];
|
|
57
79
|
beforeEach(() => {
|
|
58
80
|
ProjectLogsManager.reset();
|
|
81
|
+
isV2Project.mockReturnValue(false);
|
|
59
82
|
getProjectConfig.mockResolvedValue(projectConfig);
|
|
60
83
|
ensureProjectExists.mockResolvedValue(projectDetails);
|
|
61
84
|
fetchProjectComponentsMetadata.mockResolvedValue({
|
|
@@ -69,7 +92,7 @@ describe('lib/projects/ProjectLogsManager', () => {
|
|
|
69
92
|
appId,
|
|
70
93
|
},
|
|
71
94
|
featureComponents: [
|
|
72
|
-
...
|
|
95
|
+
...legacyApiFunctions,
|
|
73
96
|
{
|
|
74
97
|
type: {
|
|
75
98
|
name: 'NOT_AN_APP_FUNCTION',
|
|
@@ -128,6 +151,102 @@ describe('lib/projects/ProjectLogsManager', () => {
|
|
|
128
151
|
expect(ProjectLogsManager.functions).toEqual(functions);
|
|
129
152
|
});
|
|
130
153
|
});
|
|
154
|
+
describe('v2 project init', () => {
|
|
155
|
+
const v2ProjectConfig = {
|
|
156
|
+
projectConfig: {
|
|
157
|
+
name: projectName,
|
|
158
|
+
srcDir: 'src',
|
|
159
|
+
platformVersion: '2025.2',
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
const deployedBuildId = 555;
|
|
163
|
+
const v2ProjectDetails = {
|
|
164
|
+
project: {
|
|
165
|
+
id: projectId,
|
|
166
|
+
deployedBuild: {
|
|
167
|
+
buildId: deployedBuildId,
|
|
168
|
+
subbuildStatuses: {},
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
const appUid = 'my-app';
|
|
173
|
+
const fnUid1 = 'my-app/app.functions/function1';
|
|
174
|
+
const fnUid2 = 'my-app/app.functions/function2';
|
|
175
|
+
const deployedNodes = {
|
|
176
|
+
[appUid]: {
|
|
177
|
+
componentType: 'APPLICATION',
|
|
178
|
+
componentDeps: {},
|
|
179
|
+
metaFilePath: 'src/app/app-hsmeta.json',
|
|
180
|
+
uid: appUid,
|
|
181
|
+
config: {},
|
|
182
|
+
files: {},
|
|
183
|
+
},
|
|
184
|
+
[fnUid1]: {
|
|
185
|
+
componentType: 'APP_FUNCTION',
|
|
186
|
+
componentDeps: { app: appUid },
|
|
187
|
+
metaFilePath: 'src/app/app.functions/function1.functions/function-hsmeta.json',
|
|
188
|
+
uid: fnUid1,
|
|
189
|
+
config: { endpoint: { path: '/my-endpoint' } },
|
|
190
|
+
files: {},
|
|
191
|
+
},
|
|
192
|
+
[fnUid2]: {
|
|
193
|
+
componentType: 'APP_FUNCTION',
|
|
194
|
+
componentDeps: { app: appUid },
|
|
195
|
+
metaFilePath: 'src/app/app.functions/function2.functions/function-hsmeta.json',
|
|
196
|
+
uid: fnUid2,
|
|
197
|
+
config: {},
|
|
198
|
+
files: {},
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
beforeEach(() => {
|
|
202
|
+
getProjectConfig.mockResolvedValue(v2ProjectConfig);
|
|
203
|
+
ensureProjectExists.mockResolvedValue(v2ProjectDetails);
|
|
204
|
+
isV2Project.mockReturnValue(true);
|
|
205
|
+
getDeployedProjectNodes.mockResolvedValue(deployedNodes);
|
|
206
|
+
fetchAppMetadataBySourceId.mockResolvedValue({
|
|
207
|
+
data: { id: appId },
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
it('should populate functions correctly for v2 projects', async () => {
|
|
211
|
+
await ProjectLogsManager.init(accountId);
|
|
212
|
+
expect(getDeployedProjectNodes).toHaveBeenCalledWith(v2ProjectConfig.projectConfig, accountId, deployedBuildId);
|
|
213
|
+
expect(fetchAppMetadataBySourceId).toHaveBeenCalledWith(projectId, appUid, accountId);
|
|
214
|
+
expect(ProjectLogsManager.functions).toEqual([
|
|
215
|
+
{
|
|
216
|
+
componentName: fnUid1,
|
|
217
|
+
appId,
|
|
218
|
+
endpoint: { path: '/my-endpoint' },
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
componentName: fnUid2,
|
|
222
|
+
appId,
|
|
223
|
+
endpoint: undefined,
|
|
224
|
+
},
|
|
225
|
+
]);
|
|
226
|
+
});
|
|
227
|
+
it('should throw noDeployedBuild when buildId is missing', async () => {
|
|
228
|
+
ensureProjectExists.mockResolvedValue({
|
|
229
|
+
project: {
|
|
230
|
+
id: projectId,
|
|
231
|
+
deployedBuild: {
|
|
232
|
+
buildId: undefined,
|
|
233
|
+
subbuildStatuses: {},
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
await expect(async () => ProjectLogsManager.init(accountId)).rejects.toThrow('This project has not been deployed yet. Deploy the project first, then try again.');
|
|
238
|
+
});
|
|
239
|
+
it('should throw noFunctionsInProject when no function nodes exist', async () => {
|
|
240
|
+
getDeployedProjectNodes.mockResolvedValue({
|
|
241
|
+
[appUid]: deployedNodes[appUid],
|
|
242
|
+
});
|
|
243
|
+
await expect(async () => ProjectLogsManager.init(accountId)).rejects.toThrow(/There aren't any functions in this project/);
|
|
244
|
+
});
|
|
245
|
+
it('should throw a user-friendly error when getDeployedProjectNodes fails', async () => {
|
|
246
|
+
getDeployedProjectNodes.mockRejectedValue(new Error('download failed'));
|
|
247
|
+
await expect(async () => ProjectLogsManager.init(accountId)).rejects.toThrow(/There was an error fetching project details/);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
131
250
|
describe('getFunctionNames', () => {
|
|
132
251
|
it('should return an empty array if functions is empty', async () => {
|
|
133
252
|
ProjectLogsManager.functions = [];
|
|
@@ -154,14 +273,8 @@ describe('lib/projects/ProjectLogsManager', () => {
|
|
|
154
273
|
it('should set the data correctly for public functions', async () => {
|
|
155
274
|
const functionToChoose = {
|
|
156
275
|
componentName: 'function1',
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
},
|
|
160
|
-
deployOutput: {
|
|
161
|
-
appId: 123,
|
|
162
|
-
appFunctionName: 'function1',
|
|
163
|
-
endpoint: { path: 'yooooooo', methods: ['GET'] },
|
|
164
|
-
},
|
|
276
|
+
appId: 123,
|
|
277
|
+
endpoint: { path: 'yooooooo' },
|
|
165
278
|
};
|
|
166
279
|
ProjectLogsManager.functions = [functionToChoose];
|
|
167
280
|
ProjectLogsManager.setFunction('function1');
|
|
@@ -170,7 +283,7 @@ describe('lib/projects/ProjectLogsManager', () => {
|
|
|
170
283
|
expect(ProjectLogsManager.selectedFunction).toEqual(functionToChoose);
|
|
171
284
|
expect(ProjectLogsManager.isPublicFunction).toEqual(true);
|
|
172
285
|
});
|
|
173
|
-
it('should set the data correctly for
|
|
286
|
+
it('should set the data correctly for private functions', async () => {
|
|
174
287
|
ProjectLogsManager.functions = functions;
|
|
175
288
|
ProjectLogsManager.setFunction('function1');
|
|
176
289
|
expect(ProjectLogsManager.selectedFunction).toEqual(function1);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isV2Project } from '../platformVersion.js';
|
|
1
|
+
import { isV2Project, isUnsupportedPlatformVersion, } from '../platformVersion.js';
|
|
2
2
|
describe('platformVersion', () => {
|
|
3
3
|
describe('isV2Project', () => {
|
|
4
4
|
it('returns true if platform version is UNSTABLE', () => {
|
|
@@ -24,4 +24,40 @@ describe('platformVersion', () => {
|
|
|
24
24
|
expect(isV2Project('notplaformversion')).toBe(false);
|
|
25
25
|
});
|
|
26
26
|
});
|
|
27
|
+
describe('isUnsupportedPlatformVersion', () => {
|
|
28
|
+
it('returns false for platform version 2026.03 (boundary)', () => {
|
|
29
|
+
expect(isUnsupportedPlatformVersion('2026.03')).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
it('returns false for platform versions less than 2026.03', () => {
|
|
32
|
+
expect(isUnsupportedPlatformVersion('2025.2')).toBe(false);
|
|
33
|
+
expect(isUnsupportedPlatformVersion('2026.01')).toBe(false);
|
|
34
|
+
expect(isUnsupportedPlatformVersion('2026.02')).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
it('returns true for platform version 2026.04', () => {
|
|
37
|
+
expect(isUnsupportedPlatformVersion('2026.04')).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
it('returns true for platform versions greater than 2026.03', () => {
|
|
40
|
+
expect(isUnsupportedPlatformVersion('2026.4')).toBe(true);
|
|
41
|
+
expect(isUnsupportedPlatformVersion('2027.01')).toBe(true);
|
|
42
|
+
expect(isUnsupportedPlatformVersion('2027.1')).toBe(true);
|
|
43
|
+
expect(isUnsupportedPlatformVersion('2028.10')).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
it('returns false for UNSTABLE', () => {
|
|
46
|
+
expect(isUnsupportedPlatformVersion('UNSTABLE')).toBe(false);
|
|
47
|
+
expect(isUnsupportedPlatformVersion('unstable')).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
it('returns false for null or undefined', () => {
|
|
50
|
+
expect(isUnsupportedPlatformVersion(null)).toBe(false);
|
|
51
|
+
expect(isUnsupportedPlatformVersion(undefined)).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
it('returns false for invalid platform versions', () => {
|
|
54
|
+
expect(isUnsupportedPlatformVersion('notaversion')).toBe(false);
|
|
55
|
+
expect(isUnsupportedPlatformVersion('abc.def')).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
it('handles beta versions correctly', () => {
|
|
58
|
+
expect(isUnsupportedPlatformVersion('2026.03-beta')).toBe(false);
|
|
59
|
+
expect(isUnsupportedPlatformVersion('2026.04-beta')).toBe(true);
|
|
60
|
+
expect(isUnsupportedPlatformVersion('2027.01-beta')).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
27
63
|
});
|
|
@@ -16,12 +16,16 @@ describe('lib/projects', () => {
|
|
|
16
16
|
});
|
|
17
17
|
it('rejects configuration with missing name', () => {
|
|
18
18
|
// @ts-ignore Testing invalid input
|
|
19
|
-
expect(() => validateProjectConfig({ srcDir: '.' }, projectDir)).toThrow(
|
|
19
|
+
expect(() => validateProjectConfig({ srcDir: '.' }, projectDir)).toThrow(/missing required field.*name/);
|
|
20
20
|
});
|
|
21
21
|
it('rejects configuration with missing srcDir', () => {
|
|
22
22
|
expect(() =>
|
|
23
23
|
// @ts-ignore Testing invalid input
|
|
24
|
-
validateProjectConfig({ name: 'hello' }, projectDir)).toThrow(
|
|
24
|
+
validateProjectConfig({ name: 'hello' }, projectDir)).toThrow(/missing required field.*srcDir/);
|
|
25
|
+
});
|
|
26
|
+
it('rejects configuration with both name and srcDir missing', () => {
|
|
27
|
+
// @ts-ignore Testing invalid input
|
|
28
|
+
expect(() => validateProjectConfig({}, projectDir)).toThrow(/missing required fields:.*name.*srcDir/);
|
|
25
29
|
});
|
|
26
30
|
describe('rejects configuration with srcDir outside project directory', () => {
|
|
27
31
|
it('for parent directory', () => {
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { Collision } from '@hubspot/local-dev-lib/types/Archive';
|
|
2
2
|
import type { ProjectMetadata } from '@hubspot/project-parsing-lib/projects';
|
|
3
|
+
export interface ComponentInfo {
|
|
4
|
+
filename?: string;
|
|
5
|
+
isNew: boolean;
|
|
6
|
+
}
|
|
7
|
+
export type ComponentsByType = Map<string, ComponentInfo[]>;
|
|
8
|
+
export declare function buildProjectTree(projectName: string, uids: string[], componentsByType: ComponentsByType, showOnlyNew: boolean): string;
|
|
3
9
|
export declare function handleComponentCollision({ dest, src, collisions }: Collision): void;
|
|
4
10
|
export declare function updateHsMetaFilesWithAutoGeneratedFields(projectName: string, hsMetaFilePaths: string[], existingUids?: string[], options?: {
|
|
5
11
|
currentProjectMetadata?: ProjectMetadata;
|
|
@@ -12,7 +12,7 @@ import { renderInline } from '../../ui/render.js';
|
|
|
12
12
|
import { getSuccessBox } from '../../ui/components/StatusMessageBoxes.js';
|
|
13
13
|
// Prefix for the metafile extension
|
|
14
14
|
const metafileExtensionPrefix = path.parse(metafileExtension).name;
|
|
15
|
-
function buildProjectTree(projectName, uids, componentsByType, showOnlyNew) {
|
|
15
|
+
export function buildProjectTree(projectName, uids, componentsByType, showOnlyNew) {
|
|
16
16
|
const lines = [];
|
|
17
17
|
lines.push(chalk.bold(projectName));
|
|
18
18
|
const types = Array.from(componentsByType.keys());
|