@hubspot/ui-extensions-dev-server 0.10.1 → 1.0.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.
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 +73 -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 +4 -9
  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 +22 -39
  95. package/package.json +44 -31
@@ -0,0 +1,256 @@
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
+ default: () => {
9
+ return {
10
+ name: 'ui-extensions-relevant-modules-plugin',
11
+ };
12
+ },
13
+ };
14
+ });
15
+ vi.mock('vite');
16
+ import devBuildPlugin from "../../plugins/devBuildPlugin.js";
17
+ import { build } from 'vite';
18
+ import { PLATFORM_VERSION, WEBSOCKET_MESSAGE_VERSION, } from "../../constants.js";
19
+ import { DevServerState } from "../../DevServerState.js";
20
+ import { transformedUnifiedCardOneConfig } from "../fixtures/extensionConfig.js";
21
+ import { urls } from "../fixtures/urls.js";
22
+ describe('devBuildPlugin', () => {
23
+ let options;
24
+ let plugin;
25
+ let server;
26
+ let logger;
27
+ let mockWebSocket;
28
+ beforeEach(() => {
29
+ logger = {
30
+ info: vi.fn(),
31
+ error: vi.fn(),
32
+ debug: vi.fn(),
33
+ warn: vi.fn(),
34
+ };
35
+ // Create mock ExtensionsWebSocket
36
+ mockWebSocket = {
37
+ broadcast: vi.fn(),
38
+ onConnection: vi.fn(),
39
+ clientCount: 1,
40
+ };
41
+ options = {
42
+ devServerState: new DevServerState({
43
+ extensionConfigs: [transformedUnifiedCardOneConfig],
44
+ accountId: 88888,
45
+ platformVersion: PLATFORM_VERSION.V20232,
46
+ expressPort: 1234,
47
+ logger,
48
+ urls,
49
+ }),
50
+ };
51
+ // Set the mock websocket on the devServerState
52
+ // @ts-expect-error Setting private property for testing
53
+ options.devServerState._mutableState.extensionsWebSocket =
54
+ mockWebSocket;
55
+ plugin = devBuildPlugin(options);
56
+ server = {};
57
+ });
58
+ afterEach(() => {
59
+ vi.clearAllMocks();
60
+ });
61
+ describe('metadata', () => {
62
+ it('should create the correct plugin metadata', () => {
63
+ expect(plugin).toStrictEqual(expect.objectContaining({
64
+ name: 'ui-extensions-dev-build-plugin',
65
+ enforce: 'pre',
66
+ configureServer: expect.any(Function),
67
+ handleHotUpdate: expect.any(Function),
68
+ buildEnd: expect.any(Function),
69
+ }));
70
+ });
71
+ });
72
+ describe('configureServer', () => {
73
+ it('should setup event handlers and perform initial build', async () => {
74
+ // @ts-expect-error TS thinks these aren't functions
75
+ await plugin.configureServer(server);
76
+ options.devServerState.triggerWebSocketSetup();
77
+ expect(mockWebSocket.onConnection).toHaveBeenCalledTimes(1);
78
+ expect(mockWebSocket.onConnection).toHaveBeenCalledWith(expect.any(Function));
79
+ // Should also perform initial build for all extensions
80
+ expect(build).toHaveBeenCalledTimes(1);
81
+ });
82
+ it('should broadcast start messages when a client connects', async () => {
83
+ // @ts-expect-error TS thinks these aren't functions
84
+ await plugin.configureServer(server);
85
+ options.devServerState.triggerWebSocketSetup();
86
+ // Get the connection handler that was registered
87
+ const connectionHandler = mockWebSocket.onConnection.mock.calls[0][0];
88
+ // Simulate a connection
89
+ connectionHandler();
90
+ const baseMessage = options.devServerState.extensionsMetadata[0].baseMessage;
91
+ expect(mockWebSocket.broadcast).toHaveBeenCalledWith({
92
+ event: 'start',
93
+ ...baseMessage,
94
+ version: WEBSOCKET_MESSAGE_VERSION,
95
+ });
96
+ expect(logger.info).toHaveBeenCalledWith('Browser connected and listening for bundle updates');
97
+ });
98
+ });
99
+ describe('handleHotUpdate', () => {
100
+ it('should trigger a vite build', async () => {
101
+ // @ts-expect-error TS thinks these aren't functions
102
+ await plugin.handleHotUpdate({ file: 'extension.js', server });
103
+ expect(build).toHaveBeenCalledTimes(1);
104
+ const extensionConfig = options.devServerState.extensionsMetadata[0].config;
105
+ expect(build).toHaveBeenCalledWith(expect.objectContaining({
106
+ mode: 'development',
107
+ logLevel: 'warn',
108
+ clearScreen: false,
109
+ define: expect.objectContaining({
110
+ 'process.env.NODE_ENV': expect.any(String),
111
+ }),
112
+ esbuild: expect.objectContaining({
113
+ tsconfigRaw: expect.any(Object),
114
+ }),
115
+ build: expect.objectContaining({
116
+ lib: {
117
+ entry: extensionConfig.data.module.file,
118
+ name: extensionConfig.output,
119
+ formats: ['iife'],
120
+ fileName: expect.any(Function),
121
+ },
122
+ rollupOptions: expect.objectContaining({
123
+ plugins: expect.arrayContaining([
124
+ expect.objectContaining({
125
+ name: 'ui-extensions-manifest-generation-plugin',
126
+ }),
127
+ expect.objectContaining({
128
+ name: 'ui-extensions-code-checking-plugin',
129
+ }),
130
+ expect.objectContaining({
131
+ name: 'ui-extensions-friendly-logging-plugin',
132
+ }),
133
+ expect.objectContaining({
134
+ name: 'ui-extensions-relevant-modules-plugin',
135
+ }),
136
+ expect.objectContaining({
137
+ name: 'ui-extensions-code-blocking-plugin',
138
+ }),
139
+ ]),
140
+ }),
141
+ outDir: options.devServerState.outputDir,
142
+ emptyOutDir: false,
143
+ minify: false,
144
+ sourcemap: 'inline',
145
+ }),
146
+ }));
147
+ });
148
+ it('should broadcast error message on build failure', async () => {
149
+ const error = {
150
+ plugin: 'vite:esbuild',
151
+ errors: ['you did something wrong'],
152
+ frame: 'It broke on this line',
153
+ loc: {
154
+ column: 1,
155
+ line: 5,
156
+ },
157
+ id: 'this is the file where things broke',
158
+ };
159
+ vi.mocked(build).mockImplementationOnce(() => {
160
+ // eslint-disable-next-line no-throw-literal
161
+ throw error;
162
+ });
163
+ // @ts-expect-error TS thinks these aren't functions
164
+ await plugin.handleHotUpdate({ file: 'extension.js', server });
165
+ expect(mockWebSocket.broadcast).toHaveBeenCalled();
166
+ const baseMessage = options.devServerState.extensionsMetadata[0].baseMessage;
167
+ expect(mockWebSocket.broadcast).toHaveBeenCalledWith({
168
+ event: 'error',
169
+ ...baseMessage,
170
+ error: {
171
+ details: {
172
+ errors: error.errors,
173
+ file: error.id,
174
+ formattedError: error.frame,
175
+ location: error.loc,
176
+ },
177
+ },
178
+ version: WEBSOCKET_MESSAGE_VERSION,
179
+ });
180
+ });
181
+ it('should not broadcast when error is from ui-extensions plugin', async () => {
182
+ const error = {
183
+ plugin: 'ui-extensions-some-plugin-that-threw-an-error',
184
+ };
185
+ vi.mocked(build).mockImplementationOnce(() => {
186
+ throw error;
187
+ });
188
+ // @ts-expect-error TS thinks these aren't functions
189
+ await plugin.handleHotUpdate({ file: 'extension.js', server });
190
+ expect(mockWebSocket.broadcast).not.toHaveBeenCalled();
191
+ });
192
+ it('should not trigger a build if it is not a relevant file', async () => {
193
+ const file = 'card config file';
194
+ plugin = devBuildPlugin({
195
+ ...options,
196
+ });
197
+ // @ts-expect-error TS thinks these aren't functions
198
+ await plugin.handleHotUpdate({ server, file });
199
+ expect(build).not.toHaveBeenCalled();
200
+ });
201
+ it('should not broadcast if there are no connected clients', async () => {
202
+ // Set clientCount to 0
203
+ mockWebSocket.clientCount = 0;
204
+ // @ts-expect-error TS thinks these aren't functions
205
+ await plugin.handleHotUpdate({
206
+ server,
207
+ file: 'extension.js',
208
+ });
209
+ expect(mockWebSocket.broadcast).not.toHaveBeenCalled();
210
+ expect(logger.debug).toHaveBeenCalledWith('Bundle updated, no browsers connected to notify');
211
+ });
212
+ it('should broadcast update message to connected clients on build success', async () => {
213
+ // @ts-expect-error TS thinks these aren't functions
214
+ await plugin.handleHotUpdate({
215
+ server,
216
+ file: 'extension.js',
217
+ });
218
+ expect(mockWebSocket.broadcast).toHaveBeenCalledTimes(1);
219
+ const baseMessage = options.devServerState.extensionsMetadata[0].baseMessage;
220
+ expect(mockWebSocket.broadcast).toHaveBeenCalledWith({
221
+ event: 'update',
222
+ ...baseMessage,
223
+ version: WEBSOCKET_MESSAGE_VERSION,
224
+ });
225
+ expect(logger.debug).toHaveBeenCalledWith('Bundle updated, notifying connected browsers');
226
+ });
227
+ });
228
+ describe('buildEnd', () => {
229
+ it('should log an error if one is provided', () => {
230
+ const error = 'Error message';
231
+ // @ts-expect-error TS thinks these aren't functions
232
+ plugin.buildEnd(error);
233
+ expect(logger.error).toHaveBeenCalledWith(error);
234
+ });
235
+ it('should broadcast shutdown message when websocket is available', () => {
236
+ // @ts-expect-error TS thinks these aren't functions
237
+ plugin.buildEnd(null);
238
+ const baseMessage = options.devServerState.extensionsMetadata[0].baseMessage;
239
+ expect(mockWebSocket.broadcast).toHaveBeenCalledWith({
240
+ event: 'shutdown',
241
+ ...baseMessage,
242
+ version: WEBSOCKET_MESSAGE_VERSION,
243
+ });
244
+ expect(logger.debug).toHaveBeenCalledWith('Sending shutdown message to connected browsers');
245
+ });
246
+ it('should not crash if websocket is not initialized', () => {
247
+ // Remove the websocket
248
+ // @ts-expect-error Setting private property for testing
249
+ options.devServerState._mutableState.extensionsWebSocket = null;
250
+ expect(() => {
251
+ // @ts-expect-error TS thinks these aren't functions
252
+ plugin.buildEnd(null);
253
+ }).not.toThrow();
254
+ });
255
+ });
256
+ });
@@ -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.');
63
+ });
64
+ });
65
+ });