@hubspot/ui-extensions-dev-server 1.1.0 → 1.1.2

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/dist/index.d.ts +4 -0
  2. package/dist/index.js +4 -0
  3. package/dist/lib/DevModeInterface.d.ts +9 -0
  4. package/dist/lib/DevModeInterface.js +36 -0
  5. package/dist/lib/DevModeParentInterface.d.ts +19 -0
  6. package/dist/lib/DevModeParentInterface.js +181 -0
  7. package/dist/lib/DevModeUnifiedInterface.d.ts +9 -0
  8. package/dist/lib/DevModeUnifiedInterface.js +118 -0
  9. package/dist/lib/DevServerState.d.ts +44 -0
  10. package/dist/lib/DevServerState.js +95 -0
  11. package/dist/lib/ExtensionsWebSocket.d.ts +25 -0
  12. package/dist/lib/ExtensionsWebSocket.js +110 -0
  13. package/dist/lib/__mocks__/config.d.ts +2 -0
  14. package/dist/lib/__mocks__/config.js +5 -0
  15. package/dist/lib/__mocks__/isExtensionFile.d.ts +5 -0
  16. package/dist/lib/__mocks__/isExtensionFile.js +11 -0
  17. package/dist/lib/__tests__/DevModeInterface.spec.d.ts +1 -0
  18. package/dist/lib/__tests__/DevModeInterface.spec.js +155 -0
  19. package/dist/lib/__tests__/DevModeParentInterface.spec.d.ts +1 -0
  20. package/dist/lib/__tests__/DevModeParentInterface.spec.js +179 -0
  21. package/dist/lib/__tests__/DevModeUnifiedInterface.spec.d.ts +1 -0
  22. package/dist/lib/__tests__/DevModeUnifiedInterface.spec.js +236 -0
  23. package/dist/lib/__tests__/ExtensionsWebSocket.spec.d.ts +1 -0
  24. package/dist/lib/__tests__/ExtensionsWebSocket.spec.js +304 -0
  25. package/dist/lib/__tests__/ast.spec.d.ts +1 -0
  26. package/dist/lib/__tests__/ast.spec.js +737 -0
  27. package/dist/lib/__tests__/build.spec.d.ts +1 -0
  28. package/dist/lib/__tests__/build.spec.js +159 -0
  29. package/dist/lib/__tests__/config.spec.d.ts +1 -0
  30. package/dist/lib/__tests__/config.spec.js +291 -0
  31. package/dist/lib/__tests__/dev.spec.d.ts +1 -0
  32. package/dist/lib/__tests__/dev.spec.js +80 -0
  33. package/dist/lib/__tests__/extensionsService.spec.d.ts +1 -0
  34. package/dist/lib/__tests__/extensionsService.spec.js +150 -0
  35. package/dist/lib/__tests__/factories.d.ts +48 -0
  36. package/dist/lib/__tests__/factories.js +32 -0
  37. package/dist/lib/__tests__/fixtures/extensionConfig.d.ts +182 -0
  38. package/dist/lib/__tests__/fixtures/extensionConfig.js +304 -0
  39. package/dist/lib/__tests__/fixtures/urls.d.ts +4 -0
  40. package/dist/lib/__tests__/fixtures/urls.js +4 -0
  41. package/dist/lib/__tests__/parsing-utils.spec.d.ts +1 -0
  42. package/dist/lib/__tests__/parsing-utils.spec.js +467 -0
  43. package/dist/lib/__tests__/plugins/codeBlockingPlugin.spec.d.ts +1 -0
  44. package/dist/lib/__tests__/plugins/codeBlockingPlugin.spec.js +112 -0
  45. package/dist/lib/__tests__/plugins/codeCheckingPlugin.spec.d.ts +1 -0
  46. package/dist/lib/__tests__/plugins/codeCheckingPlugin.spec.js +124 -0
  47. package/dist/lib/__tests__/plugins/devBuildPlugin.spec.d.ts +1 -0
  48. package/dist/lib/__tests__/plugins/devBuildPlugin.spec.js +396 -0
  49. package/dist/lib/__tests__/plugins/friendlyLoggingPlugin.spec.d.ts +1 -0
  50. package/dist/lib/__tests__/plugins/friendlyLoggingPlugin.spec.js +65 -0
  51. package/dist/lib/__tests__/plugins/manifestPlugin.spec.d.ts +1 -0
  52. package/dist/lib/__tests__/plugins/manifestPlugin.spec.js +455 -0
  53. package/dist/lib/__tests__/plugins/relevantModulesPlugin.spec.d.ts +1 -0
  54. package/dist/lib/__tests__/plugins/relevantModulesPlugin.spec.js +115 -0
  55. package/dist/lib/__tests__/server.spec.d.ts +1 -0
  56. package/dist/lib/__tests__/server.spec.js +152 -0
  57. package/dist/lib/__tests__/test-utils/ast.d.ts +1 -0
  58. package/dist/lib/__tests__/test-utils/ast.js +4 -0
  59. package/dist/lib/__tests__/utils.spec.d.ts +1 -0
  60. package/dist/lib/__tests__/utils.spec.js +176 -0
  61. package/dist/lib/ast.d.ts +16 -0
  62. package/dist/lib/ast.js +281 -0
  63. package/dist/lib/bin/cli.d.ts +2 -0
  64. package/dist/lib/bin/cli.js +143 -0
  65. package/dist/lib/build.d.ts +24 -0
  66. package/dist/lib/build.js +73 -0
  67. package/dist/lib/config.d.ts +7 -0
  68. package/dist/lib/config.js +124 -0
  69. package/dist/lib/constants.d.ts +32 -0
  70. package/dist/lib/constants.js +43 -0
  71. package/dist/lib/dev.d.ts +2 -0
  72. package/dist/lib/dev.js +58 -0
  73. package/dist/lib/extensionsService.d.ts +10 -0
  74. package/dist/lib/extensionsService.js +45 -0
  75. package/dist/lib/parsing-utils.d.ts +31 -0
  76. package/dist/lib/parsing-utils.js +289 -0
  77. package/dist/lib/plugins/codeBlockingPlugin.d.ts +8 -0
  78. package/dist/lib/plugins/codeBlockingPlugin.js +45 -0
  79. package/dist/lib/plugins/codeCheckingPlugin.d.ts +8 -0
  80. package/dist/lib/plugins/codeCheckingPlugin.js +93 -0
  81. package/dist/lib/plugins/devBuildPlugin.d.ts +8 -0
  82. package/dist/lib/plugins/devBuildPlugin.js +212 -0
  83. package/dist/lib/plugins/friendlyLoggingPlugin.d.ts +14 -0
  84. package/dist/lib/plugins/friendlyLoggingPlugin.js +36 -0
  85. package/dist/lib/plugins/manifestPlugin.d.ts +12 -0
  86. package/dist/lib/plugins/manifestPlugin.js +158 -0
  87. package/dist/lib/plugins/relevantModulesPlugin.d.ts +14 -0
  88. package/dist/lib/plugins/relevantModulesPlugin.js +33 -0
  89. package/dist/lib/server.d.ts +13 -0
  90. package/dist/lib/server.js +99 -0
  91. package/dist/lib/types.d.ts +290 -0
  92. package/dist/lib/types.js +12 -0
  93. package/dist/lib/utils.d.ts +25 -0
  94. package/dist/lib/utils.js +113 -0
  95. package/package.json +2 -1
@@ -0,0 +1,124 @@
1
+ import { tmpdir } from 'os';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { build } from 'vite';
5
+ import { describe, expect, it, vi } from 'vitest';
6
+ import codeCheckingPlugin from "../../plugins/codeCheckingPlugin.js";
7
+ import { ROLLUP_OPTIONS } from "../../constants.js";
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+ const fixturesDir = path.join(__dirname, 'fixtures/codeCheckingPlugin');
11
+ async function runViteBuild(fixtureFile, plugin) {
12
+ const fixturePath = path.join(fixturesDir, fixtureFile);
13
+ const outputDir = path.join(tmpdir(), `vite-test-${Date.now()}-${Math.random().toString(36).substring(7)}`);
14
+ await build({
15
+ logLevel: 'silent',
16
+ root: fixturesDir,
17
+ build: {
18
+ lib: {
19
+ entry: fixturePath,
20
+ name: 'test',
21
+ formats: ['es'],
22
+ fileName: () => 'test.js',
23
+ },
24
+ rollupOptions: {
25
+ ...ROLLUP_OPTIONS,
26
+ external: [
27
+ ...(ROLLUP_OPTIONS.external || []),
28
+ '@hubspot/ui-extensions',
29
+ 'other-package',
30
+ ],
31
+ plugins: [plugin],
32
+ },
33
+ outDir: outputDir,
34
+ emptyOutDir: true,
35
+ write: false,
36
+ },
37
+ });
38
+ }
39
+ const createPlugin = (options) => {
40
+ const logger = options?.logger || {
41
+ info: vi.fn(),
42
+ error: vi.fn(),
43
+ debug: vi.fn(),
44
+ warn: vi.fn(),
45
+ };
46
+ return {
47
+ logger,
48
+ plugin: codeCheckingPlugin({ logger, ...options }),
49
+ };
50
+ };
51
+ describe('codeCheckingPlugin', () => {
52
+ describe('with warning', () => {
53
+ it('should log a warning when entry point has no imports from @hubspot/ui-extensions', async () => {
54
+ const { logger, plugin } = createPlugin();
55
+ await runViteBuild('emptyFile.js', plugin);
56
+ expect(logger.warn).toHaveBeenCalledTimes(1);
57
+ const warnCall = logger.warn.mock
58
+ .calls[0][0];
59
+ expect(warnCall).toContain('WARNING:');
60
+ expect(warnCall).toContain('hubspot');
61
+ expect(warnCall).toContain('@hubspot/ui-extensions');
62
+ });
63
+ it('should log a warning when code does not contain @hubspot/ui-extensions package at all', async () => {
64
+ const { logger, plugin } = createPlugin();
65
+ await runViteBuild('withoutHubspotPackage.js', plugin);
66
+ expect(logger.warn).toHaveBeenCalledTimes(1);
67
+ });
68
+ it('should log a warning when code imports from @hubspot/ui-extensions but does not import hubspot', async () => {
69
+ const { logger, plugin } = createPlugin();
70
+ await runViteBuild('withoutHubspotImport.js', plugin);
71
+ expect(logger.warn).toHaveBeenCalledTimes(1);
72
+ });
73
+ });
74
+ describe('without warning', () => {
75
+ it('should not log a warning when hubspot is imported from a separate entry point', async () => {
76
+ const { logger, plugin } = createPlugin();
77
+ await runViteBuild('withHubspotImportInSeparateEntry.js', plugin);
78
+ expect(logger.warn).not.toHaveBeenCalled();
79
+ });
80
+ it('should not log a warning when hubspot is imported from @hubspot/ui-extensions', async () => {
81
+ const { logger, plugin } = createPlugin();
82
+ await runViteBuild('withHubspotImport.js', plugin);
83
+ expect(logger.warn).not.toHaveBeenCalled();
84
+ });
85
+ it('should not log a warning when hubspot is imported alongside other exports from @hubspot/ui-extensions', async () => {
86
+ const { logger, plugin } = createPlugin();
87
+ await runViteBuild('withHubspotAndOtherExports.js', plugin);
88
+ expect(logger.warn).not.toHaveBeenCalled();
89
+ });
90
+ it('should not log a warning when hubspot is imported with other exports in any order', async () => {
91
+ const { logger, plugin } = createPlugin();
92
+ await runViteBuild('withHubspotInMixedExports.js', plugin);
93
+ expect(logger.warn).not.toHaveBeenCalled();
94
+ });
95
+ it('should not log a warning when hubspot is imported using single quotes', async () => {
96
+ const { logger, plugin } = createPlugin();
97
+ await runViteBuild('withHubspotImportSingleQuotes.js', plugin);
98
+ expect(logger.warn).not.toHaveBeenCalled();
99
+ });
100
+ it('should not log a warning when using namespace import from @hubspot/ui-extensions', async () => {
101
+ const { logger, plugin } = createPlugin();
102
+ await runViteBuild('withNamespaceImport.js', plugin);
103
+ expect(logger.warn).not.toHaveBeenCalled();
104
+ });
105
+ it('should not log a warning when hubspot is imported in TypeScript files', async () => {
106
+ const { logger, plugin } = createPlugin();
107
+ await runViteBuild('ts-withHubspotImport.ts', plugin);
108
+ expect(logger.warn).not.toHaveBeenCalled();
109
+ });
110
+ it('should not log a warning when the build fails with an error', async () => {
111
+ const { logger, plugin } = createPlugin();
112
+ // Use a fixture file that would normally trigger the warning,
113
+ // but cause a build error by importing a non-existent file
114
+ try {
115
+ await runViteBuild('invalidSyntax.js', plugin);
116
+ }
117
+ catch (error) {
118
+ // Build should fail, which is expected
119
+ }
120
+ // Verify warning was not logged despite the fixture missing hubspot import
121
+ expect(logger.warn).not.toHaveBeenCalled();
122
+ });
123
+ });
124
+ });
@@ -0,0 +1,396 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ vi.mock('../../plugins/relevantModulesPlugin', () => {
3
+ return {
4
+ __esModule: true,
5
+ getRelevantModules: () => {
6
+ return ['extension.js', 'helper-file.js'];
7
+ },
8
+ addRelevantModule: vi.fn(),
9
+ default: () => {
10
+ return {
11
+ name: 'ui-extensions-relevant-modules-plugin',
12
+ };
13
+ },
14
+ };
15
+ });
16
+ vi.mock('vite');
17
+ import devBuildPlugin from "../../plugins/devBuildPlugin.js";
18
+ import { addRelevantModule } from "../../plugins/relevantModulesPlugin.js";
19
+ import { build } from 'vite';
20
+ import { PLATFORM_VERSION, WEBSOCKET_MESSAGE_VERSION, } from "../../constants.js";
21
+ import { DevServerState } from "../../DevServerState.js";
22
+ import { transformedUnifiedCardOneConfig } from "../fixtures/extensionConfig.js";
23
+ import { urls } from "../fixtures/urls.js";
24
+ describe('devBuildPlugin', () => {
25
+ let options;
26
+ let plugin;
27
+ let server;
28
+ let logger;
29
+ let mockWebSocket;
30
+ beforeEach(() => {
31
+ logger = {
32
+ info: vi.fn(),
33
+ error: vi.fn(),
34
+ debug: vi.fn(),
35
+ warn: vi.fn(),
36
+ };
37
+ // Create mock ExtensionsWebSocket
38
+ mockWebSocket = {
39
+ broadcast: vi.fn(),
40
+ onConnection: vi.fn(),
41
+ clientCount: 1,
42
+ };
43
+ options = {
44
+ devServerState: new DevServerState({
45
+ extensionConfigs: [transformedUnifiedCardOneConfig],
46
+ accountId: 88888,
47
+ platformVersion: PLATFORM_VERSION.V20232,
48
+ expressPort: 1234,
49
+ logger,
50
+ urls,
51
+ }),
52
+ };
53
+ // Set the mock websocket on the devServerState
54
+ // @ts-expect-error Setting private property for testing
55
+ options.devServerState._mutableState.extensionsWebSocket =
56
+ mockWebSocket;
57
+ plugin = devBuildPlugin(options);
58
+ server = {};
59
+ });
60
+ afterEach(() => {
61
+ vi.clearAllMocks();
62
+ });
63
+ describe('metadata', () => {
64
+ it('should create the correct plugin metadata', () => {
65
+ expect(plugin).toStrictEqual(expect.objectContaining({
66
+ name: 'ui-extensions-dev-build-plugin',
67
+ enforce: 'pre',
68
+ configureServer: expect.any(Function),
69
+ handleHotUpdate: expect.any(Function),
70
+ buildEnd: expect.any(Function),
71
+ }));
72
+ });
73
+ });
74
+ describe('configureServer', () => {
75
+ it('should setup event handlers and perform initial build', async () => {
76
+ // @ts-expect-error TS thinks these aren't functions
77
+ await plugin.configureServer(server);
78
+ options.devServerState.triggerWebSocketSetup();
79
+ expect(mockWebSocket.onConnection).toHaveBeenCalledTimes(1);
80
+ expect(mockWebSocket.onConnection).toHaveBeenCalledWith(expect.any(Function));
81
+ // Should also perform initial build for all extensions
82
+ expect(build).toHaveBeenCalledTimes(1);
83
+ });
84
+ it('should broadcast start messages when a client connects', async () => {
85
+ // @ts-expect-error TS thinks these aren't functions
86
+ await plugin.configureServer(server);
87
+ options.devServerState.triggerWebSocketSetup();
88
+ // Get the connection handler that was registered
89
+ const connectionHandler = mockWebSocket.onConnection.mock.calls[0][0];
90
+ // Simulate a connection
91
+ connectionHandler();
92
+ const baseMessage = options.devServerState.extensionsMetadata[0].baseMessage;
93
+ expect(mockWebSocket.broadcast).toHaveBeenCalledWith({
94
+ event: 'start',
95
+ ...baseMessage,
96
+ version: WEBSOCKET_MESSAGE_VERSION,
97
+ });
98
+ expect(logger.info).toHaveBeenCalledWith('Browser connected and listening for bundle updates');
99
+ });
100
+ });
101
+ describe('handleHotUpdate', () => {
102
+ it('should trigger a vite build', async () => {
103
+ // @ts-expect-error TS thinks these aren't functions
104
+ await plugin.handleHotUpdate({ file: 'extension.js', server });
105
+ expect(build).toHaveBeenCalledTimes(1);
106
+ const extensionConfig = options.devServerState.extensionsMetadata[0].config;
107
+ expect(build).toHaveBeenCalledWith(expect.objectContaining({
108
+ mode: 'development',
109
+ logLevel: 'warn',
110
+ clearScreen: false,
111
+ define: expect.objectContaining({
112
+ 'process.env.NODE_ENV': expect.any(String),
113
+ }),
114
+ esbuild: expect.objectContaining({
115
+ tsconfigRaw: expect.any(Object),
116
+ }),
117
+ build: expect.objectContaining({
118
+ lib: {
119
+ entry: extensionConfig.data.module.file,
120
+ name: extensionConfig.output,
121
+ formats: ['iife'],
122
+ fileName: expect.any(Function),
123
+ },
124
+ rollupOptions: expect.objectContaining({
125
+ plugins: expect.arrayContaining([
126
+ expect.objectContaining({
127
+ name: 'ui-extensions-manifest-generation-plugin',
128
+ }),
129
+ expect.objectContaining({
130
+ name: 'ui-extensions-code-checking-plugin',
131
+ }),
132
+ expect.objectContaining({
133
+ name: 'ui-extensions-friendly-logging-plugin',
134
+ }),
135
+ expect.objectContaining({
136
+ name: 'ui-extensions-relevant-modules-plugin',
137
+ }),
138
+ expect.objectContaining({
139
+ name: 'ui-extensions-code-blocking-plugin',
140
+ }),
141
+ ]),
142
+ }),
143
+ outDir: options.devServerState.outputDir,
144
+ emptyOutDir: false,
145
+ minify: false,
146
+ sourcemap: 'inline',
147
+ }),
148
+ }));
149
+ });
150
+ it('should broadcast error message on build failure', async () => {
151
+ const error = {
152
+ plugin: 'vite:esbuild',
153
+ message: '[vite:esbuild] Transform failed with 1 error',
154
+ errors: ['you did something wrong'],
155
+ frame: 'It broke on this line',
156
+ loc: {
157
+ column: 1,
158
+ line: 5,
159
+ },
160
+ id: 'this is the file where things broke',
161
+ };
162
+ vi.mocked(build).mockImplementationOnce(() => {
163
+ // eslint-disable-next-line no-throw-literal
164
+ throw error;
165
+ });
166
+ // @ts-expect-error TS thinks these aren't functions
167
+ await plugin.handleHotUpdate({ file: 'extension.js', server });
168
+ expect(logger.error).toHaveBeenCalledWith(error.message);
169
+ // Verify entry file and error file are added to relevant modules for HMR
170
+ const extensionConfig = options.devServerState.extensionsMetadata[0].config;
171
+ const entryFile = extensionConfig.data.module.file;
172
+ expect(vi.mocked(addRelevantModule)).toHaveBeenCalledWith(extensionConfig.output, entryFile);
173
+ expect(vi.mocked(addRelevantModule)).toHaveBeenCalledWith(extensionConfig.output, error.id);
174
+ expect(mockWebSocket.broadcast).toHaveBeenCalled();
175
+ const baseMessage = options.devServerState.extensionsMetadata[0].baseMessage;
176
+ expect(mockWebSocket.broadcast).toHaveBeenCalledWith({
177
+ event: 'error',
178
+ ...baseMessage,
179
+ error: {
180
+ details: {
181
+ errors: error.errors,
182
+ file: error.id,
183
+ formattedError: error.frame,
184
+ location: error.loc,
185
+ },
186
+ },
187
+ version: WEBSOCKET_MESSAGE_VERSION,
188
+ });
189
+ });
190
+ it('should add only entry file when error file is same as entry file', async () => {
191
+ const extensionConfig = options.devServerState.extensionsMetadata[0].config;
192
+ const entryFile = extensionConfig.data.module.file;
193
+ const error = {
194
+ plugin: 'vite:esbuild',
195
+ message: '[vite:esbuild] Transform failed with 1 error',
196
+ errors: ['syntax error in entry file'],
197
+ frame: 'Error on this line',
198
+ loc: { column: 1, line: 5 },
199
+ id: entryFile, // Same as entry file
200
+ };
201
+ vi.mocked(build).mockImplementationOnce(() => {
202
+ throw error;
203
+ });
204
+ // @ts-expect-error TS thinks these aren't functions
205
+ await plugin.handleHotUpdate({ file: 'extension.js', server });
206
+ // Verify entry file is added
207
+ expect(vi.mocked(addRelevantModule)).toHaveBeenCalledWith(extensionConfig.output, entryFile);
208
+ // Verify addRelevantModule is only called once (not twice for the same file)
209
+ expect(vi.mocked(addRelevantModule)).toHaveBeenCalledTimes(1);
210
+ });
211
+ it('should not broadcast when error is from ui-extensions plugin', async () => {
212
+ const error = {
213
+ plugin: 'ui-extensions-some-plugin-that-threw-an-error',
214
+ };
215
+ vi.mocked(build).mockImplementationOnce(() => {
216
+ throw error;
217
+ });
218
+ // @ts-expect-error TS thinks these aren't functions
219
+ await plugin.handleHotUpdate({ file: 'extension.js', server });
220
+ expect(mockWebSocket.broadcast).not.toHaveBeenCalled();
221
+ });
222
+ it('should not trigger a build if it is not a relevant file', async () => {
223
+ const file = 'card config file';
224
+ plugin = devBuildPlugin({
225
+ ...options,
226
+ });
227
+ // @ts-expect-error TS thinks these aren't functions
228
+ await plugin.handleHotUpdate({ server, file });
229
+ expect(build).not.toHaveBeenCalled();
230
+ });
231
+ it('should not broadcast if there are no connected clients', async () => {
232
+ // Set clientCount to 0
233
+ mockWebSocket.clientCount = 0;
234
+ // @ts-expect-error TS thinks these aren't functions
235
+ await plugin.handleHotUpdate({
236
+ server,
237
+ file: 'extension.js',
238
+ });
239
+ expect(mockWebSocket.broadcast).not.toHaveBeenCalled();
240
+ expect(logger.debug).toHaveBeenCalledWith('Bundle updated, no browsers connected to notify');
241
+ });
242
+ it('should broadcast update message to connected clients on build success', async () => {
243
+ // @ts-expect-error TS thinks these aren't functions
244
+ await plugin.handleHotUpdate({
245
+ server,
246
+ file: 'extension.js',
247
+ });
248
+ expect(mockWebSocket.broadcast).toHaveBeenCalledTimes(1);
249
+ const baseMessage = options.devServerState.extensionsMetadata[0].baseMessage;
250
+ expect(mockWebSocket.broadcast).toHaveBeenCalledWith({
251
+ event: 'update',
252
+ ...baseMessage,
253
+ version: WEBSOCKET_MESSAGE_VERSION,
254
+ });
255
+ expect(logger.debug).toHaveBeenCalledWith('Bundle updated, notifying connected browsers');
256
+ });
257
+ });
258
+ describe('buildEnd', () => {
259
+ it('should log an error if one is provided', () => {
260
+ const error = 'Error message';
261
+ // @ts-expect-error TS thinks these aren't functions
262
+ plugin.buildEnd(error);
263
+ expect(logger.error).toHaveBeenCalledWith(error);
264
+ });
265
+ it('should broadcast shutdown message when websocket is available', () => {
266
+ // @ts-expect-error TS thinks these aren't functions
267
+ plugin.buildEnd(null);
268
+ const baseMessage = options.devServerState.extensionsMetadata[0].baseMessage;
269
+ expect(mockWebSocket.broadcast).toHaveBeenCalledWith({
270
+ event: 'shutdown',
271
+ ...baseMessage,
272
+ version: WEBSOCKET_MESSAGE_VERSION,
273
+ });
274
+ expect(logger.debug).toHaveBeenCalledWith('Sending shutdown message to connected browsers');
275
+ });
276
+ it('should not crash if websocket is not initialized', () => {
277
+ // Remove the websocket
278
+ // @ts-expect-error Setting private property for testing
279
+ options.devServerState._mutableState.extensionsWebSocket = null;
280
+ expect(() => {
281
+ // @ts-expect-error TS thinks these aren't functions
282
+ plugin.buildEnd(null);
283
+ }).not.toThrow();
284
+ });
285
+ });
286
+ describe('handleBuildError', () => {
287
+ it('should not crash if websocket is not initialized during configureServer', async () => {
288
+ // Remove the websocket to simulate it not being initialized yet
289
+ // @ts-expect-error Setting private property for testing
290
+ options.devServerState._mutableState.extensionsWebSocket = null;
291
+ const error = {
292
+ plugin: 'vite:esbuild',
293
+ message: '[vite:esbuild] Build error',
294
+ errors: ['build error'],
295
+ frame: 'Error on this line',
296
+ loc: { column: 1, line: 5 },
297
+ id: 'src/file.ts',
298
+ };
299
+ vi.mocked(build).mockImplementationOnce(() => {
300
+ throw error;
301
+ });
302
+ // Should not throw even though websocket doesn't exist
303
+ await expect(
304
+ // @ts-expect-error TS thinks this isn't a function
305
+ plugin.configureServer(server)).resolves.not.toThrow();
306
+ expect(logger.error).toHaveBeenCalledWith(error.message);
307
+ // Verify entry file and error file are added to relevant modules
308
+ const extensionConfig = options.devServerState.extensionsMetadata[0].config;
309
+ const entryFile = extensionConfig.data.module.file;
310
+ expect(vi.mocked(addRelevantModule)).toHaveBeenCalledWith(extensionConfig.output, entryFile);
311
+ expect(vi.mocked(addRelevantModule)).toHaveBeenCalledWith(extensionConfig.output, error.id);
312
+ // Verify broadcast was NOT called (since websocket doesn't exist)
313
+ expect(mockWebSocket.broadcast).not.toHaveBeenCalled();
314
+ });
315
+ it('should broadcast error when websocket exists and browser connects', async () => {
316
+ // Websocket is already set up in beforeEach
317
+ const error = {
318
+ plugin: 'vite:esbuild',
319
+ message: '[vite:esbuild] Build error',
320
+ errors: ['build error'],
321
+ frame: 'Error on this line',
322
+ loc: { column: 1, line: 5 },
323
+ id: 'src/file.ts',
324
+ };
325
+ vi.mocked(build).mockImplementationOnce(() => {
326
+ throw error;
327
+ });
328
+ // @ts-expect-error TS thinks this isn't a function
329
+ await plugin.configureServer(server);
330
+ expect(logger.error).toHaveBeenCalledWith(error.message);
331
+ // Verify entry file and error file are added to relevant modules
332
+ const extensionConfig = options.devServerState.extensionsMetadata[0].config;
333
+ const entryFile = extensionConfig.data.module.file;
334
+ expect(vi.mocked(addRelevantModule)).toHaveBeenCalledWith(extensionConfig.output, entryFile);
335
+ expect(vi.mocked(addRelevantModule)).toHaveBeenCalledWith(extensionConfig.output, error.id);
336
+ options.devServerState.triggerWebSocketSetup();
337
+ // Get the connection handler
338
+ const connectionHandler = mockWebSocket.onConnection.mock.calls[0][0];
339
+ // Simulate browser connection - should broadcast the stored error
340
+ connectionHandler();
341
+ expect(mockWebSocket.broadcast).toHaveBeenCalledWith(expect.objectContaining({
342
+ event: 'error',
343
+ error: expect.objectContaining({
344
+ details: expect.objectContaining({
345
+ file: error.id,
346
+ }),
347
+ }),
348
+ }));
349
+ });
350
+ it('should broadcast error after websocket is initialized and browser connects', async () => {
351
+ // Remove the websocket to simulate it not being initialized yet
352
+ // @ts-expect-error Setting private property for testing
353
+ options.devServerState._mutableState.extensionsWebSocket = null;
354
+ const error = {
355
+ plugin: 'vite:esbuild',
356
+ message: '[vite:esbuild] Build error',
357
+ errors: ['build error'],
358
+ frame: 'Error on this line',
359
+ loc: { column: 1, line: 5 },
360
+ id: 'src/file.ts',
361
+ };
362
+ vi.mocked(build).mockImplementationOnce(() => {
363
+ throw error;
364
+ });
365
+ // configureServer runs with error, but websocket doesn't exist yet
366
+ // @ts-expect-error TS thinks this isn't a function
367
+ await plugin.configureServer(server);
368
+ expect(logger.error).toHaveBeenCalledWith(error.message);
369
+ // Verify entry file and error file are added to relevant modules
370
+ const extensionConfig = options.devServerState.extensionsMetadata[0].config;
371
+ const entryFile = extensionConfig.data.module.file;
372
+ expect(vi.mocked(addRelevantModule)).toHaveBeenCalledWith(extensionConfig.output, entryFile);
373
+ expect(vi.mocked(addRelevantModule)).toHaveBeenCalledWith(extensionConfig.output, error.id);
374
+ // At this point, broadcast should not have been called
375
+ expect(mockWebSocket.broadcast).not.toHaveBeenCalled();
376
+ // Now initialize the websocket (simulating what happens in server.ts)
377
+ // @ts-expect-error Setting private property for testing
378
+ options.devServerState._mutableState.extensionsWebSocket =
379
+ mockWebSocket;
380
+ // Trigger the websocket setup callback
381
+ options.devServerState.triggerWebSocketSetup();
382
+ // Get the connection handler
383
+ const connectionHandler = mockWebSocket.onConnection.mock.calls[0][0];
384
+ // Simulate browser connection - should NOW broadcast the stored error
385
+ connectionHandler();
386
+ expect(mockWebSocket.broadcast).toHaveBeenCalledWith(expect.objectContaining({
387
+ event: 'error',
388
+ error: expect.objectContaining({
389
+ details: expect.objectContaining({
390
+ file: error.id,
391
+ }),
392
+ }),
393
+ }));
394
+ });
395
+ });
396
+ });
@@ -0,0 +1,65 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import friendlyLoggingPlugin from "../../plugins/friendlyLoggingPlugin.js";
3
+ describe('friendlyLoggingPlugin', () => {
4
+ let plugin;
5
+ let logger;
6
+ beforeEach(() => {
7
+ logger = {
8
+ info: vi.fn(),
9
+ error: vi.fn(),
10
+ debug: vi.fn(),
11
+ warn: vi.fn(),
12
+ };
13
+ plugin = friendlyLoggingPlugin({ logger });
14
+ });
15
+ afterEach(() => {
16
+ vi.clearAllMocks();
17
+ });
18
+ describe('metadata', () => {
19
+ it('should create the correct plugin metadata', () => {
20
+ expect(plugin).toStrictEqual(expect.objectContaining({
21
+ name: 'ui-extensions-friendly-logging-plugin',
22
+ enforce: 'post',
23
+ onLog: expect.any(Function),
24
+ }));
25
+ });
26
+ });
27
+ describe('onLog', () => {
28
+ it('should return true if it is a log message we do not want altered', () => {
29
+ // @ts-expect-error TS doesn't think lifecycle hooks are functions
30
+ const result = plugin.onLog('error', {
31
+ code: "it's an older code but it checks out",
32
+ });
33
+ expect(result).toBe(true);
34
+ });
35
+ it('should return false if it is a log message we want to transform', () => {
36
+ // @ts-expect-error TS doesn't think lifecycle hooks are functions
37
+ const result = plugin.onLog('error', {
38
+ code: 'MISSING_GLOBAL_NAME',
39
+ });
40
+ expect(result).toBe(false);
41
+ });
42
+ it('should not log MISSING_GLOBAL_NAME errors', () => {
43
+ // @ts-expect-error TS doesn't think lifecycle hooks are functions
44
+ const result = plugin.onLog('error', {
45
+ code: 'MISSING_GLOBAL_NAME',
46
+ });
47
+ expect(result).toBe(false);
48
+ expect(logger.error).toHaveBeenCalledTimes(0);
49
+ expect(logger.info).toHaveBeenCalledTimes(0);
50
+ expect(logger.debug).toHaveBeenCalledTimes(0);
51
+ expect(logger.warn).toHaveBeenCalledTimes(0);
52
+ });
53
+ it('should call the logger correctly for UNRESOLVED_IMPORT errors', () => {
54
+ // @ts-expect-error TS doesn't think lifecycle hooks are functions
55
+ const result = plugin.onLog('we ignore this for now', {
56
+ code: 'UNRESOLVED_IMPORT',
57
+ id: 'the/file/that/imported/the/bad/import.jsx',
58
+ exporter: '@hubspot/extensions',
59
+ });
60
+ expect(result).toBe(false);
61
+ expect(logger.error).toHaveBeenCalledTimes(1);
62
+ expect(logger.error).toHaveBeenCalledWith('@hubspot/extensions is imported by import.jsx, but @hubspot/extensions cannot be resolved. Make sure @hubspot/extensions is installed by running `hs project install-deps`.');
63
+ });
64
+ });
65
+ });