@hubspot/ui-extensions-dev-server 1.1.1 → 1.1.3

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.
@@ -107,5 +107,18 @@ describe('codeCheckingPlugin', () => {
107
107
  await runViteBuild('ts-withHubspotImport.ts', plugin);
108
108
  expect(logger.warn).not.toHaveBeenCalled();
109
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
+ });
110
123
  });
111
124
  });
@@ -5,6 +5,7 @@ vi.mock('../../plugins/relevantModulesPlugin', () => {
5
5
  getRelevantModules: () => {
6
6
  return ['extension.js', 'helper-file.js'];
7
7
  },
8
+ addRelevantModule: vi.fn(),
8
9
  default: () => {
9
10
  return {
10
11
  name: 'ui-extensions-relevant-modules-plugin',
@@ -14,6 +15,7 @@ vi.mock('../../plugins/relevantModulesPlugin', () => {
14
15
  });
15
16
  vi.mock('vite');
16
17
  import devBuildPlugin from "../../plugins/devBuildPlugin.js";
18
+ import { addRelevantModule } from "../../plugins/relevantModulesPlugin.js";
17
19
  import { build } from 'vite';
18
20
  import { PLATFORM_VERSION, WEBSOCKET_MESSAGE_VERSION, } from "../../constants.js";
19
21
  import { DevServerState } from "../../DevServerState.js";
@@ -148,6 +150,7 @@ describe('devBuildPlugin', () => {
148
150
  it('should broadcast error message on build failure', async () => {
149
151
  const error = {
150
152
  plugin: 'vite:esbuild',
153
+ message: '[vite:esbuild] Transform failed with 1 error',
151
154
  errors: ['you did something wrong'],
152
155
  frame: 'It broke on this line',
153
156
  loc: {
@@ -162,6 +165,12 @@ describe('devBuildPlugin', () => {
162
165
  });
163
166
  // @ts-expect-error TS thinks these aren't functions
164
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);
165
174
  expect(mockWebSocket.broadcast).toHaveBeenCalled();
166
175
  const baseMessage = options.devServerState.extensionsMetadata[0].baseMessage;
167
176
  expect(mockWebSocket.broadcast).toHaveBeenCalledWith({
@@ -178,6 +187,27 @@ describe('devBuildPlugin', () => {
178
187
  version: WEBSOCKET_MESSAGE_VERSION,
179
188
  });
180
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
+ });
181
211
  it('should not broadcast when error is from ui-extensions plugin', async () => {
182
212
  const error = {
183
213
  plugin: 'ui-extensions-some-plugin-that-threw-an-error',
@@ -260,6 +290,7 @@ describe('devBuildPlugin', () => {
260
290
  options.devServerState._mutableState.extensionsWebSocket = null;
261
291
  const error = {
262
292
  plugin: 'vite:esbuild',
293
+ message: '[vite:esbuild] Build error',
263
294
  errors: ['build error'],
264
295
  frame: 'Error on this line',
265
296
  loc: { column: 1, line: 5 },
@@ -272,6 +303,12 @@ describe('devBuildPlugin', () => {
272
303
  await expect(
273
304
  // @ts-expect-error TS thinks this isn't a function
274
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);
275
312
  // Verify broadcast was NOT called (since websocket doesn't exist)
276
313
  expect(mockWebSocket.broadcast).not.toHaveBeenCalled();
277
314
  });
@@ -279,6 +316,7 @@ describe('devBuildPlugin', () => {
279
316
  // Websocket is already set up in beforeEach
280
317
  const error = {
281
318
  plugin: 'vite:esbuild',
319
+ message: '[vite:esbuild] Build error',
282
320
  errors: ['build error'],
283
321
  frame: 'Error on this line',
284
322
  loc: { column: 1, line: 5 },
@@ -289,6 +327,12 @@ describe('devBuildPlugin', () => {
289
327
  });
290
328
  // @ts-expect-error TS thinks this isn't a function
291
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);
292
336
  options.devServerState.triggerWebSocketSetup();
293
337
  // Get the connection handler
294
338
  const connectionHandler = mockWebSocket.onConnection.mock.calls[0][0];
@@ -309,6 +353,7 @@ describe('devBuildPlugin', () => {
309
353
  options.devServerState._mutableState.extensionsWebSocket = null;
310
354
  const error = {
311
355
  plugin: 'vite:esbuild',
356
+ message: '[vite:esbuild] Build error',
312
357
  errors: ['build error'],
313
358
  frame: 'Error on this line',
314
359
  loc: { column: 1, line: 5 },
@@ -320,6 +365,12 @@ describe('devBuildPlugin', () => {
320
365
  // configureServer runs with error, but websocket doesn't exist yet
321
366
  // @ts-expect-error TS thinks this isn't a function
322
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);
323
374
  // At this point, broadcast should not have been called
324
375
  expect(mockWebSocket.broadcast).not.toHaveBeenCalled();
325
376
  // Now initialize the websocket (simulating what happens in server.ts)
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import relevantModulesPlugin, { getRelevantModules, } from "../../plugins/relevantModulesPlugin.js";
2
+ import relevantModulesPlugin, { getRelevantModules, addRelevantModule, } from "../../plugins/relevantModulesPlugin.js";
3
3
  describe('manifestPlugin', () => {
4
4
  let logger;
5
5
  let options;
@@ -49,6 +49,40 @@ describe('manifestPlugin', () => {
49
49
  expect(actual).toStrictEqual([]);
50
50
  });
51
51
  });
52
+ describe('addRelevantModule', () => {
53
+ it('should add module to empty list', () => {
54
+ const output = 'TestOutput.js';
55
+ const modulePath = '/path/to/module.js';
56
+ addRelevantModule(output, modulePath);
57
+ const actual = getRelevantModules(output);
58
+ expect(actual).toStrictEqual([modulePath]);
59
+ });
60
+ it('should add module to existing list', () => {
61
+ const output = 'AnotherOutput.js';
62
+ addRelevantModule(output, 'module1.js');
63
+ addRelevantModule(output, 'module2.js');
64
+ const actual = getRelevantModules(output);
65
+ expect(actual).toStrictEqual(['module1.js', 'module2.js']);
66
+ });
67
+ it('should not add duplicate modules', () => {
68
+ const output = 'DuplicateTest.js';
69
+ const modulePath = '/path/to/module.js';
70
+ addRelevantModule(output, modulePath);
71
+ addRelevantModule(output, modulePath);
72
+ addRelevantModule(output, modulePath);
73
+ const actual = getRelevantModules(output);
74
+ expect(actual).toStrictEqual([modulePath]);
75
+ });
76
+ it('should create output entry if it does not exist', () => {
77
+ const output = 'NewOutput.js';
78
+ const modulePath = '/path/to/new/module.js';
79
+ // Verify output doesn't exist yet
80
+ expect(getRelevantModules(output)).toStrictEqual([]);
81
+ addRelevantModule(output, modulePath);
82
+ // Verify output was created with the module
83
+ expect(getRelevantModules(output)).toStrictEqual([modulePath]);
84
+ });
85
+ });
52
86
  describe('generateBundle', () => {
53
87
  it('should not update the relevantModules when output is not found in the bundle', () => {
54
88
  const plugin = relevantModulesPlugin(options);
@@ -64,7 +64,11 @@ const codeCheckingPlugin = (options) => {
64
64
  * If not found, we log a warning to help developers understand why their extension
65
65
  * might not render correctly.
66
66
  */
67
- buildEnd() {
67
+ buildEnd(error) {
68
+ // Don't display the warning if there was a build error
69
+ if (error) {
70
+ return;
71
+ }
68
72
  if (!foundHubspotImport) {
69
73
  logger.warn(`
70
74
 
@@ -4,7 +4,7 @@ import manifestPlugin from "./manifestPlugin.js";
4
4
  import { stripAnsiColorCodes } from "../utils.js";
5
5
  import codeCheckingPlugin from "./codeCheckingPlugin.js";
6
6
  import friendlyLoggingPlugin from "./friendlyLoggingPlugin.js";
7
- import relevantModulesPlugin, { getRelevantModules, } from "./relevantModulesPlugin.js";
7
+ import relevantModulesPlugin, { getRelevantModules, addRelevantModule, } from "./relevantModulesPlugin.js";
8
8
  import codeBlockingPlugin from "./codeBlockingPlugin.js";
9
9
  function addVersionToBaseMessage(baseMessage) {
10
10
  return {
@@ -52,9 +52,9 @@ const devBuildPlugin = (options) => {
52
52
  }
53
53
  };
54
54
  const devBuild = async (server, extensionMetadata, emptyOutDir = false) => {
55
+ const { config: extensionConfig } = extensionMetadata;
56
+ const { extensionPath } = extensionConfig;
55
57
  try {
56
- const { config: extensionConfig } = extensionMetadata;
57
- const { extensionPath } = extensionConfig;
58
58
  let manifestConfig = {};
59
59
  if (devServerState.appConfig &&
60
60
  'variables' in devServerState.appConfig &&
@@ -122,7 +122,18 @@ const devBuildPlugin = (options) => {
122
122
  error: error,
123
123
  extensionMetadata,
124
124
  };
125
- logger.debug(error);
125
+ // Log the error for user visibility
126
+ const errorPayload = error;
127
+ if (errorPayload.message) {
128
+ logger.error(errorPayload.message);
129
+ }
130
+ // Ensure the files are tracked for HMR even when the build fails
131
+ const entryFile = extensionConfig.data.module.file;
132
+ addRelevantModule(extensionConfig.output, entryFile);
133
+ if (errorPayload.id && errorPayload.id !== entryFile) {
134
+ addRelevantModule(extensionConfig.output, errorPayload.id);
135
+ }
136
+ logger.debug('Full build error:', error);
126
137
  handleBuildError(lastBuildErrorContext);
127
138
  return false;
128
139
  }
@@ -4,6 +4,7 @@ export interface RelevantModules {
4
4
  [key: string]: string[];
5
5
  }
6
6
  export declare function getRelevantModules(output: string): string[];
7
+ export declare function addRelevantModule(output: string, modulePath: string): void;
7
8
  export interface RelevantModulesPluginOptions {
8
9
  output: string;
9
10
  logger: Logger;
@@ -3,6 +3,14 @@ const relevantModules = {};
3
3
  export function getRelevantModules(output) {
4
4
  return relevantModules[output] || [];
5
5
  }
6
+ export function addRelevantModule(output, modulePath) {
7
+ if (!relevantModules[output]) {
8
+ relevantModules[output] = [];
9
+ }
10
+ if (!relevantModules[output].includes(modulePath)) {
11
+ relevantModules[output].push(modulePath);
12
+ }
13
+ }
6
14
  const relevantModulesPlugin = ({ output, logger }) => {
7
15
  return {
8
16
  name: 'ui-extensions-relevant-modules-plugin',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/ui-extensions-dev-server",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,6 +12,7 @@
12
12
  "build": "npm run clean && tsc",
13
13
  "watch": "npm run clean && tsc --watch",
14
14
  "prepare": "husky",
15
+ "prepublishOnly": "npm run build",
15
16
  "prettier:write": "prettier --write '**/*.{ts,js,json}'",
16
17
  "lint": "echo 'Linting is disabled for Blazar'",
17
18
  "lint:local": "eslint . && prettier --check '**/*.{ts,js,json}'",
@@ -32,7 +33,7 @@
32
33
  ],
33
34
  "license": "MIT",
34
35
  "dependencies": {
35
- "@hubspot/app-functions-dev-server": "0.11.0",
36
+ "@hubspot/app-functions-dev-server": "0.11.1",
36
37
  "chalk": "5.4.1",
37
38
  "commander": "13.1.0",
38
39
  "cors": "2.8.5",