@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
@@ -1,88 +1,70 @@
1
- "use strict";
2
- /* hs-eslint ignored failing-rules */
3
- /* eslint-disable hubspot-dev/no-unsupported-ts-syntax */
4
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
5
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
6
- return new (P || (P = Promise))(function (resolve, reject) {
7
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
8
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
9
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
10
- step((generator = generator.apply(thisArg, _arguments || [])).next());
11
- });
12
- };
13
- var __importDefault = (this && this.__importDefault) || function (mod) {
14
- return (mod && mod.__esModule) ? mod : { "default": mod };
15
- };
16
- Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.DevModeUnifiedInterfaceNonSingleton = void 0;
18
- const path_1 = __importDefault(require("path"));
19
- const constants_1 = require("./constants");
20
- const types_1 = require("./types");
21
- const DevModeParentInterface_1 = require("./DevModeParentInterface");
22
- const utils_1 = require("./utils");
1
+ import path from 'path';
2
+ import { SUPPORTED_EXTENSION_TYPES } from "./constants.js";
3
+ import { UnifiedComponentTypes, } from "./types.js";
4
+ import { DevModeParentInterface } from "./DevModeParentInterface.js";
5
+ import { getUrlSafeFileName } from "./utils.js";
23
6
  function getComponentName(componentType) {
24
- if (componentType === types_1.UnifiedComponentTypes.SETTINGS) {
7
+ if (componentType === UnifiedComponentTypes.SETTINGS) {
25
8
  return 'Settings';
26
9
  }
27
- if (componentType === types_1.UnifiedComponentTypes.PAGE) {
10
+ if (componentType === UnifiedComponentTypes.PAGE) {
28
11
  return 'App Home';
29
12
  }
30
13
  }
31
- class DevModeUnifiedInterface extends DevModeParentInterface_1.DevModeParentInterface {
14
+ class DevModeUnifiedInterface extends DevModeParentInterface {
32
15
  _generateAppExtensionMappings(components, profileData) {
33
- var _a, _b, _c, _d;
34
16
  const mappings = [];
35
17
  // Loop over all of the components that are passed in
36
18
  const allComponentUids = Object.keys(components);
37
19
  // Find the app component
38
20
  const appUid = allComponentUids.find((componentUid) => {
39
21
  return (components[componentUid].componentType ===
40
- types_1.UnifiedComponentTypes.APPLICATION);
22
+ UnifiedComponentTypes.APPLICATION);
41
23
  });
42
24
  // This should fail a lot sooner (on the cli side), but added this just in case.
43
25
  if (!appUid) {
44
- this.logger.error('Application configuration is missing.');
26
+ // TODO: Add this once logger is set up on parent class
27
+ // this.logger.error('Application configuration is missing.');
45
28
  return mappings;
46
29
  }
47
30
  const appData = components[appUid];
48
31
  // Use the app data to generate the app config in the expected shape, to match old projects.
49
32
  const appConfig = {
50
33
  name: appData.config.name,
51
- description: (_a = appData.config) === null || _a === void 0 ? void 0 : _a.description,
34
+ description: appData.config?.description,
52
35
  uid: appData.uid,
53
36
  extensions: {
54
37
  crm: {
55
38
  cards: [],
56
39
  },
57
40
  },
58
- auth: (_b = appData.config) === null || _b === void 0 ? void 0 : _b.auth,
59
- support: (_c = appData.config) === null || _c === void 0 ? void 0 : _c.support,
41
+ auth: appData.config?.auth,
42
+ support: appData.config?.support,
60
43
  // All unified apps are currently "public" meaning they don't support serverless and do support a proxy.
61
44
  // The determination of what is "public" and "private" as it relates to our usage is still up in the air.
62
45
  isPublicApp: true,
63
- allowedUrls: ((_d = appData.config) === null || _d === void 0 ? void 0 : _d.permittedUrls)
46
+ allowedUrls: appData.config?.permittedUrls
64
47
  ? Object.values(appData.config.permittedUrls).flat()
65
48
  : [],
66
49
  variables: profileData || {},
67
50
  };
68
51
  // Then get all the supported extensions
69
52
  const extensionUids = allComponentUids.filter((componentUid) => {
70
- return constants_1.SUPPORTED_EXTENSION_TYPES.includes(components[componentUid].componentType);
53
+ return SUPPORTED_EXTENSION_TYPES.includes(components[componentUid].componentType);
71
54
  });
72
55
  // Build the extension mapping data
73
56
  extensionUids.forEach((extensionUid) => {
74
- var _a;
75
57
  const extension = components[extensionUid];
76
58
  // Update the extension entrypoint to be "relative" to the extension directory (eg from /app/card/card.jsx to ./card.jsx)
77
- extension.config.entrypoint = `./${path_1.default.basename(extension.config.entrypoint)}`;
59
+ extension.config.entrypoint = `./${path.basename(extension.config.entrypoint)}`;
78
60
  // Hardcode the extension name if this is a settings extension (the user does not provide a name for their settings card).
79
- if (extension.componentType === types_1.UnifiedComponentTypes.SETTINGS ||
80
- extension.componentType === types_1.UnifiedComponentTypes.PAGE) {
81
- extension.config.name = (_a = getComponentName(extension.componentType)) !== null && _a !== void 0 ? _a : '';
61
+ if (extension.componentType === UnifiedComponentTypes.SETTINGS ||
62
+ extension.componentType === UnifiedComponentTypes.PAGE) {
63
+ extension.config.name = getComponentName(extension.componentType) ?? '';
82
64
  }
83
65
  // Add them to the app config
84
66
  switch (extension.componentType) {
85
- case types_1.UnifiedComponentTypes.CARD:
67
+ case UnifiedComponentTypes.CARD:
86
68
  default:
87
69
  appConfig.extensions.crm.cards.push({
88
70
  file: extension.config.entrypoint,
@@ -90,8 +72,8 @@ class DevModeUnifiedInterface extends DevModeParentInterface_1.DevModeParentInte
90
72
  }
91
73
  // Generate the name and other extension data for the mapping.
92
74
  const extensionName = `${appData.config.name}/${extension.config.name}`;
93
- const extensionOutput = (0, utils_1.getUrlSafeFileName)(path_1.default.resolve(extension.localDev.componentRoot, extension.config.entrypoint));
94
- const filePath = path_1.default.resolve(extension.localDev.componentRoot, extension.config.entrypoint);
75
+ const extensionOutput = getUrlSafeFileName(path.resolve(extension.localDev.componentRoot, extension.config.entrypoint));
76
+ const filePath = path.resolve(extension.localDev.componentRoot, extension.config.entrypoint);
95
77
  // Build the config in the correct shape
96
78
  const extensionData = {
97
79
  title: extension.config.name,
@@ -127,13 +109,10 @@ class DevModeUnifiedInterface extends DevModeParentInterface_1.DevModeParentInte
127
109
  });
128
110
  return mappings;
129
111
  }
130
- setup(args) {
131
- return __awaiter(this, void 0, void 0, function* () {
132
- var _a;
133
- args.choices = this._generateAppExtensionMappings(args.components, (_a = args.profileData) !== null && _a !== void 0 ? _a : {});
134
- yield this.parentSetup(args);
135
- });
112
+ async setup(args) {
113
+ args.choices = this._generateAppExtensionMappings(args.components, args.profileData ?? {});
114
+ await this.parentSetup(args);
136
115
  }
137
116
  }
138
- exports.DevModeUnifiedInterfaceNonSingleton = DevModeUnifiedInterface;
139
- exports.default = new DevModeUnifiedInterface();
117
+ export { DevModeUnifiedInterface as DevModeUnifiedInterfaceNonSingleton };
118
+ export default new DevModeUnifiedInterface();
@@ -1,12 +1,12 @@
1
- import { AppConfig, ExtensionOutputConfig, ExtensionMetadata, Logger, PlatformVersion } from './types';
1
+ import { AppConfig, ExtensionOutputConfig, ExtensionMetadata, Logger, PlatformVersion } from './types.ts';
2
2
  import { ServiceConfiguration, ProxyServiceConfig } from '@hubspot/app-functions-dev-server';
3
+ import { ExtensionsWebSocket } from './ExtensionsWebSocket.ts';
3
4
  type DevServerStateLocalDevUrlMapping = ProxyServiceConfig['localDevUrlMapping'] | undefined;
4
5
  interface DevServerStateArgs {
5
6
  localDevUrlMapping?: DevServerStateLocalDevUrlMapping;
6
7
  extensionConfigs?: ExtensionOutputConfig[];
7
8
  accountId: number | undefined;
8
9
  expressPort: number;
9
- webSocketPort: number;
10
10
  platformVersion: PlatformVersion;
11
11
  logger: Logger;
12
12
  urls: {
@@ -16,7 +16,6 @@ interface DevServerStateArgs {
16
16
  appConfig?: AppConfig;
17
17
  }
18
18
  export declare class DevServerState {
19
- private _webSocketPort;
20
19
  private _expressPort;
21
20
  private _functionsConfig;
22
21
  private _localDevUrlMapping;
@@ -24,17 +23,22 @@ export declare class DevServerState {
24
23
  private _appPath;
25
24
  private _extensionsMetadata;
26
25
  private _portalId?;
26
+ private _mutableState;
27
27
  logger: Logger;
28
28
  appConfig?: AppConfig;
29
- constructor({ localDevUrlMapping, extensionConfigs, accountId, expressPort, webSocketPort, platformVersion, logger, urls, appConfig, }: DevServerStateArgs);
29
+ constructor({ localDevUrlMapping, extensionConfigs, accountId, expressPort, platformVersion, logger, urls, appConfig, }: DevServerStateArgs);
30
30
  get portalId(): number | undefined;
31
- get webSocketPort(): number;
32
31
  get expressPort(): number;
33
32
  get extensionsMetadata(): ExtensionMetadata[];
34
33
  get functionsConfig(): Partial<ServiceConfiguration>;
35
34
  get outputDir(): string;
36
35
  get appPath(): string;
37
36
  get localDevUrlMapping(): DevServerStateLocalDevUrlMapping;
37
+ get extensionsWebSocket(): ExtensionsWebSocket | undefined;
38
+ set extensionsWebSocket(ws: ExtensionsWebSocket);
39
+ getExtensionsWebSocket(): ExtensionsWebSocket;
38
40
  isPublicApp(): boolean;
41
+ setWebSocketSetupCallback(callback: () => void): void;
42
+ triggerWebSocketSetup(): void;
39
43
  }
40
44
  export {};
@@ -1,13 +1,17 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.DevServerState = void 0;
7
- const constants_1 = require("./constants");
8
- const path_1 = __importDefault(require("path"));
9
- class DevServerState {
10
- constructor({ localDevUrlMapping, extensionConfigs, accountId, expressPort, webSocketPort, platformVersion, logger, urls, appConfig, }) {
1
+ import { OUTPUT_DIR } from "./constants.js";
2
+ import path from 'path';
3
+ export class DevServerState {
4
+ _expressPort;
5
+ _functionsConfig;
6
+ _localDevUrlMapping;
7
+ _outputDir;
8
+ _appPath;
9
+ _extensionsMetadata;
10
+ _portalId;
11
+ _mutableState = {};
12
+ logger;
13
+ appConfig;
14
+ constructor({ localDevUrlMapping, extensionConfigs, accountId, expressPort, platformVersion, logger, urls, appConfig, }) {
11
15
  if (!extensionConfigs) {
12
16
  throw new Error('Unable to load the required extension configuration files');
13
17
  }
@@ -25,12 +29,11 @@ class DevServerState {
25
29
  },
26
30
  });
27
31
  });
28
- this._webSocketPort = webSocketPort;
29
32
  this._expressPort = expressPort;
30
33
  this._extensionsMetadata = extensionsMetadata;
31
34
  this._appPath = extensionConfigs[0].path;
32
35
  this._portalId = accountId;
33
- this._outputDir = path_1.default.join(this._appPath, constants_1.OUTPUT_DIR);
36
+ this._outputDir = path.join(this._appPath, OUTPUT_DIR);
34
37
  // Pass options from the CLI for running app functions locally
35
38
  this._functionsConfig = {
36
39
  app: { path: this._appPath },
@@ -47,9 +50,6 @@ class DevServerState {
47
50
  get portalId() {
48
51
  return this._portalId;
49
52
  }
50
- get webSocketPort() {
51
- return this._webSocketPort;
52
- }
53
53
  get expressPort() {
54
54
  return this._expressPort;
55
55
  }
@@ -68,9 +68,28 @@ class DevServerState {
68
68
  get localDevUrlMapping() {
69
69
  return this._localDevUrlMapping;
70
70
  }
71
+ get extensionsWebSocket() {
72
+ return this._mutableState.extensionsWebSocket;
73
+ }
74
+ set extensionsWebSocket(ws) {
75
+ this._mutableState.extensionsWebSocket = ws;
76
+ }
77
+ getExtensionsWebSocket() {
78
+ if (!this._mutableState.extensionsWebSocket) {
79
+ throw new Error('ExtensionsWebSocket not initialized. Server must be started first.');
80
+ }
81
+ return this._mutableState.extensionsWebSocket;
82
+ }
71
83
  isPublicApp() {
72
- var _a;
73
- return !!((_a = this.appConfig) === null || _a === void 0 ? void 0 : _a.isPublicApp);
84
+ return !!this.appConfig?.isPublicApp;
85
+ }
86
+ setWebSocketSetupCallback(callback) {
87
+ this._mutableState.webSocketSetupCallback = callback;
88
+ }
89
+ triggerWebSocketSetup() {
90
+ if (this._mutableState.webSocketSetupCallback) {
91
+ this._mutableState.webSocketSetupCallback();
92
+ this._mutableState.webSocketSetupCallback = undefined;
93
+ }
74
94
  }
75
95
  }
76
- exports.DevServerState = DevServerState;
@@ -0,0 +1,25 @@
1
+ import { Server } from 'http';
2
+ import { IncomingMessage } from 'http';
3
+ import { WebSocket } from 'ws';
4
+ import { DevServerState } from './DevServerState.ts';
5
+ export declare const ALLOWED_ORIGIN_PATTERNS: RegExp[];
6
+ export declare function isAllowedOrigin(origin: string | undefined): boolean;
7
+ type WebSocketMessage = {
8
+ event: string;
9
+ } & Record<string, unknown>;
10
+ export declare class ExtensionsWebSocket {
11
+ private wss;
12
+ private logger;
13
+ private keepAliveIntervalId?;
14
+ private clientAliveness;
15
+ constructor(httpServer: Server, devServerState: DevServerState);
16
+ private setupUpgradeHandler;
17
+ private setupErrorHandlers;
18
+ private setupClientHandlers;
19
+ private startKeepAlive;
20
+ broadcast(message: WebSocketMessage): void;
21
+ onConnection(handler: (ws: WebSocket, request: IncomingMessage) => void): void;
22
+ get clientCount(): number;
23
+ close(): Promise<void>;
24
+ }
25
+ export {};
@@ -0,0 +1,110 @@
1
+ import { WebSocket, WebSocketServer } from 'ws';
2
+ export const ALLOWED_ORIGIN_PATTERNS = [
3
+ /^https?:\/\/localhost(:\d+)?$/,
4
+ /^https?:\/\/([\w-]+\.)?hubspot(qa)?\.com(:\d+)?$/,
5
+ ];
6
+ export function isAllowedOrigin(origin) {
7
+ if (!origin)
8
+ return false;
9
+ return ALLOWED_ORIGIN_PATTERNS.some((pattern) => pattern.test(origin));
10
+ }
11
+ export class ExtensionsWebSocket {
12
+ wss;
13
+ logger;
14
+ keepAliveIntervalId;
15
+ clientAliveness = new WeakMap();
16
+ constructor(httpServer, devServerState) {
17
+ this.logger = devServerState.logger;
18
+ this.wss = new WebSocketServer({ noServer: true });
19
+ this.setupUpgradeHandler(httpServer);
20
+ this.setupErrorHandlers();
21
+ this.startKeepAlive();
22
+ }
23
+ setupUpgradeHandler(httpServer) {
24
+ httpServer.on('upgrade', (request, socket, head) => {
25
+ if (!isAllowedOrigin(request.headers.origin)) {
26
+ this.logger.debug(`Rejected WebSocket: ${request.headers.origin}`);
27
+ socket.destroy();
28
+ return;
29
+ }
30
+ this.wss.handleUpgrade(request, socket, head, (ws) => {
31
+ this.setupClientHandlers(ws);
32
+ this.wss.emit('connection', ws, request);
33
+ });
34
+ });
35
+ }
36
+ setupErrorHandlers() {
37
+ this.wss.on('error', (error) => {
38
+ this.logger.error(`WebSocket server error: ${error}`);
39
+ });
40
+ }
41
+ setupClientHandlers(ws) {
42
+ ws.on('error', (error) => {
43
+ this.logger.debug(`Client error: ${error.message}`);
44
+ });
45
+ ws.on('close', (code, reason) => {
46
+ this.logger.debug(`Client disconnected: ${code} ${reason}`);
47
+ });
48
+ ws.on('pong', () => {
49
+ this.clientAliveness.set(ws, true);
50
+ });
51
+ }
52
+ startKeepAlive() {
53
+ this.keepAliveIntervalId = setInterval(() => {
54
+ this.wss.clients.forEach((client) => {
55
+ if (this.clientAliveness.get(client) === false) {
56
+ return client.terminate();
57
+ }
58
+ this.clientAliveness.set(client, false);
59
+ client.ping();
60
+ });
61
+ }, 30000);
62
+ }
63
+ broadcast(message) {
64
+ if (this.wss.clients.size === 0) {
65
+ this.logger.debug('No clients connected, message not sent');
66
+ return;
67
+ }
68
+ const data = JSON.stringify(message);
69
+ let successCount = 0;
70
+ let failCount = 0;
71
+ this.wss.clients.forEach((client) => {
72
+ if (client.readyState === WebSocket.OPEN) {
73
+ try {
74
+ client.send(data);
75
+ successCount++;
76
+ }
77
+ catch (error) {
78
+ failCount++;
79
+ this.logger.debug(`Failed to send to client: ${error}`);
80
+ }
81
+ }
82
+ });
83
+ if (failCount > 0) {
84
+ this.logger.warn(`Sent to ${successCount} clients, ${failCount} failed`);
85
+ }
86
+ }
87
+ onConnection(handler) {
88
+ this.wss.on('connection', handler);
89
+ }
90
+ get clientCount() {
91
+ return this.wss.clients.size;
92
+ }
93
+ async close() {
94
+ if (this.keepAliveIntervalId) {
95
+ clearInterval(this.keepAliveIntervalId);
96
+ }
97
+ this.wss.clients.forEach((client) => {
98
+ this.logger.debug('Terminating WebSocket client connection');
99
+ client.terminate();
100
+ });
101
+ return new Promise((resolve, reject) => {
102
+ this.wss.close((err) => {
103
+ if (err)
104
+ reject(err);
105
+ else
106
+ resolve();
107
+ });
108
+ });
109
+ }
110
+ }
@@ -0,0 +1,2 @@
1
+ import { LocalAppConfig } from '../types.ts';
2
+ export declare const localConfig: LocalAppConfig;
@@ -0,0 +1,5 @@
1
+ export const localConfig = {
2
+ proxy: {
3
+ 'https://example.com': 'https://google.com',
4
+ },
5
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * For testing purposes, we need to mock the isExtensionFile function when not testing it directly.
3
+ * This approximates the logic without requiring files to exist on disk.
4
+ */
5
+ export declare function isExtensionFileMock(filepath: string, extensionPath: string): boolean;
@@ -0,0 +1,11 @@
1
+ import path from 'path';
2
+ /**
3
+ * For testing purposes, we need to mock the isExtensionFile function when not testing it directly.
4
+ * This approximates the logic without requiring files to exist on disk.
5
+ */
6
+ export function isExtensionFileMock(filepath, extensionPath) {
7
+ const resolvedFilePath = path.resolve(process.cwd(), filepath);
8
+ const resolvedExtensionPath = path.resolve(process.cwd(), extensionPath);
9
+ const relativePath = path.relative(resolvedExtensionPath, resolvedFilePath);
10
+ return !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
11
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,155 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach, } from 'vitest';
2
+ import DevModeInterface from "../DevModeInterface.js";
3
+ import { DevModeParentInterface } from "../DevModeParentInterface.js";
4
+ import * as config from "../config.js";
5
+ import { appComponent, appComponentTwoCards, appConfig, cardConfig, cardConfigTwo, } from "./fixtures/extensionConfig.js";
6
+ import inquirer from 'inquirer';
7
+ import { urls } from "./fixtures/urls.js";
8
+ vi.mock('../config.ts', async () => {
9
+ const actual = await vi.importActual('../config.ts');
10
+ return {
11
+ ...actual,
12
+ loadExtensionConfig: vi.fn(),
13
+ };
14
+ });
15
+ vi.mock('inquirer', () => ({
16
+ default: {
17
+ createPromptModule: vi.fn(),
18
+ },
19
+ }));
20
+ describe('DevModeInterface', () => {
21
+ describe('setup', () => {
22
+ let parentSetupSpy;
23
+ let mockPromptModule;
24
+ let setActiveApp;
25
+ let logger;
26
+ const cardKey = `${appConfig.uid}::${cardConfig.data.uid}`;
27
+ const components = {
28
+ 'my application': appComponent,
29
+ };
30
+ const expectedConfig = {
31
+ ...cardConfig,
32
+ appConfig,
33
+ };
34
+ beforeEach(() => {
35
+ DevModeInterface._reset();
36
+ logger = {
37
+ info: vi.fn(),
38
+ debug: vi.fn(),
39
+ warn: vi.fn(),
40
+ error: vi.fn(),
41
+ };
42
+ DevModeInterface.logger = logger;
43
+ mockPromptModule = vi.fn().mockResolvedValue({
44
+ extensions: [{ ...cardConfig, appConfig }],
45
+ });
46
+ vi.mocked(inquirer.createPromptModule).mockReturnValue(mockPromptModule);
47
+ setActiveApp = vi.fn(() => {
48
+ return Promise.resolve();
49
+ });
50
+ parentSetupSpy = vi.spyOn(DevModeParentInterface.prototype, 'parentSetup');
51
+ vi.mocked(config.loadExtensionConfig).mockReturnValue({
52
+ [cardKey]: cardConfig,
53
+ });
54
+ });
55
+ afterEach(() => {
56
+ vi.clearAllMocks();
57
+ });
58
+ it('should throw an error if no extensions are parsed from the provided components', async () => {
59
+ const components = {
60
+ 'not a valid application': {
61
+ config: {
62
+ name: 'I do not have an extensions object',
63
+ },
64
+ path: '/path/to/my/application',
65
+ },
66
+ };
67
+ await expect(DevModeInterface.setup({
68
+ // @ts-expect-error - Trying to trigger an error with missing fields
69
+ components,
70
+ logger,
71
+ setActiveApp,
72
+ urls,
73
+ })).rejects.toThrowError('No extensions to run');
74
+ });
75
+ it('should directly set configs when there is only one choice', async () => {
76
+ await DevModeInterface.setup({
77
+ components,
78
+ logger,
79
+ setActiveApp,
80
+ urls,
81
+ });
82
+ expect(parentSetupSpy).toHaveBeenCalledWith({
83
+ choices: [
84
+ {
85
+ name: 'my application/Meme Card',
86
+ value: expectedConfig,
87
+ },
88
+ ],
89
+ components,
90
+ logger,
91
+ setActiveApp,
92
+ urls,
93
+ });
94
+ expect(DevModeInterface.configs).toStrictEqual([expectedConfig]);
95
+ });
96
+ it('should prompt the user when there is more than once choice', async () => {
97
+ const twoCardComponents = {
98
+ 'my application': appComponentTwoCards,
99
+ };
100
+ const cardKeyTwo = `${appConfig.uid}::${cardConfigTwo.data.uid}`;
101
+ vi.mocked(config.loadExtensionConfig).mockReturnValue({
102
+ [cardKey]: cardConfig,
103
+ [cardKeyTwo]: cardConfigTwo,
104
+ });
105
+ await DevModeInterface.setup({
106
+ components: twoCardComponents,
107
+ logger,
108
+ setActiveApp,
109
+ urls,
110
+ });
111
+ expect(parentSetupSpy).toHaveBeenCalledWith({
112
+ choices: [
113
+ {
114
+ name: 'my application/Meme Card',
115
+ value: expectedConfig,
116
+ },
117
+ {
118
+ name: 'my application/Another Meme Card',
119
+ value: {
120
+ ...cardConfigTwo,
121
+ appConfig,
122
+ },
123
+ },
124
+ ],
125
+ components: twoCardComponents,
126
+ logger,
127
+ setActiveApp,
128
+ urls,
129
+ });
130
+ expect(mockPromptModule).toHaveBeenCalledTimes(1);
131
+ expect(DevModeInterface.configs).toStrictEqual([expectedConfig]);
132
+ });
133
+ it('should call setActiveApp with the correct arg', async () => {
134
+ await DevModeInterface.setup({
135
+ components,
136
+ logger,
137
+ setActiveApp,
138
+ urls,
139
+ });
140
+ expect(setActiveApp).toHaveBeenCalledTimes(1);
141
+ expect(setActiveApp).toHaveBeenCalledWith(appConfig.uid);
142
+ });
143
+ it('should not run setup if it has already been configured', async () => {
144
+ DevModeInterface.isConfigured = true;
145
+ await DevModeInterface.setup({
146
+ components,
147
+ logger,
148
+ setActiveApp,
149
+ urls,
150
+ });
151
+ expect(logger.debug).toHaveBeenCalledWith('Dev server has already been configured, skipping');
152
+ expect(DevModeInterface.configs).toBeUndefined();
153
+ });
154
+ });
155
+ });