@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.
- package/README.md +23 -4
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -45
- package/dist/lib/DevModeInterface.d.ts +2 -2
- package/dist/lib/DevModeInterface.js +12 -28
- package/dist/lib/DevModeParentInterface.d.ts +2 -2
- package/dist/lib/DevModeParentInterface.js +138 -154
- package/dist/lib/DevModeUnifiedInterface.d.ts +2 -2
- package/dist/lib/DevModeUnifiedInterface.js +28 -49
- package/dist/lib/DevServerState.d.ts +9 -5
- package/dist/lib/DevServerState.js +37 -18
- package/dist/lib/ExtensionsWebSocket.d.ts +25 -0
- package/dist/lib/ExtensionsWebSocket.js +110 -0
- package/dist/lib/__mocks__/config.d.ts +2 -0
- package/dist/lib/__mocks__/config.js +5 -0
- package/dist/lib/__mocks__/isExtensionFile.d.ts +5 -0
- package/dist/lib/__mocks__/isExtensionFile.js +11 -0
- package/dist/lib/__tests__/DevModeInterface.spec.d.ts +1 -0
- package/dist/lib/__tests__/DevModeInterface.spec.js +155 -0
- package/dist/lib/__tests__/DevModeParentInterface.spec.d.ts +1 -0
- package/dist/lib/__tests__/DevModeParentInterface.spec.js +179 -0
- package/dist/lib/__tests__/DevModeUnifiedInterface.spec.d.ts +1 -0
- package/dist/lib/__tests__/DevModeUnifiedInterface.spec.js +236 -0
- package/dist/lib/__tests__/ExtensionsWebSocket.spec.d.ts +1 -0
- package/dist/lib/__tests__/ExtensionsWebSocket.spec.js +304 -0
- package/dist/lib/__tests__/ast.spec.d.ts +1 -0
- package/dist/lib/__tests__/ast.spec.js +737 -0
- package/dist/lib/__tests__/build.spec.d.ts +1 -0
- package/dist/lib/__tests__/build.spec.js +159 -0
- package/dist/lib/__tests__/config.spec.d.ts +1 -0
- package/dist/lib/__tests__/config.spec.js +291 -0
- package/dist/lib/__tests__/dev.spec.d.ts +1 -0
- package/dist/lib/__tests__/dev.spec.js +80 -0
- package/dist/lib/__tests__/extensionsService.spec.d.ts +1 -0
- package/dist/lib/__tests__/extensionsService.spec.js +150 -0
- package/dist/lib/__tests__/factories.d.ts +48 -0
- package/dist/lib/__tests__/factories.js +32 -0
- package/dist/lib/__tests__/fixtures/extensionConfig.d.ts +182 -0
- package/dist/lib/__tests__/fixtures/extensionConfig.js +304 -0
- package/dist/lib/__tests__/fixtures/urls.d.ts +4 -0
- package/dist/lib/__tests__/fixtures/urls.js +4 -0
- package/dist/lib/__tests__/parsing-utils.spec.d.ts +1 -0
- package/dist/lib/__tests__/parsing-utils.spec.js +467 -0
- package/dist/lib/__tests__/plugins/codeBlockingPlugin.spec.d.ts +1 -0
- package/dist/lib/__tests__/plugins/codeBlockingPlugin.spec.js +112 -0
- package/dist/lib/__tests__/plugins/codeCheckingPlugin.spec.d.ts +1 -0
- package/dist/lib/__tests__/plugins/codeCheckingPlugin.spec.js +73 -0
- package/dist/lib/__tests__/plugins/devBuildPlugin.spec.d.ts +1 -0
- package/dist/lib/__tests__/plugins/devBuildPlugin.spec.js +256 -0
- package/dist/lib/__tests__/plugins/friendlyLoggingPlugin.spec.d.ts +1 -0
- package/dist/lib/__tests__/plugins/friendlyLoggingPlugin.spec.js +65 -0
- package/dist/lib/__tests__/plugins/manifestPlugin.spec.d.ts +1 -0
- package/dist/lib/__tests__/plugins/manifestPlugin.spec.js +455 -0
- package/dist/lib/__tests__/plugins/relevantModulesPlugin.spec.d.ts +1 -0
- package/dist/lib/__tests__/plugins/relevantModulesPlugin.spec.js +81 -0
- package/dist/lib/__tests__/server.spec.d.ts +1 -0
- package/dist/lib/__tests__/server.spec.js +152 -0
- package/dist/lib/__tests__/test-utils/ast.d.ts +1 -0
- package/dist/lib/__tests__/test-utils/ast.js +4 -0
- package/dist/lib/__tests__/utils.spec.d.ts +1 -0
- package/dist/lib/__tests__/utils.spec.js +176 -0
- package/dist/lib/ast.d.ts +1 -1
- package/dist/lib/ast.js +22 -29
- package/dist/lib/bin/cli.js +52 -72
- package/dist/lib/build.d.ts +1 -1
- package/dist/lib/build.js +60 -78
- package/dist/lib/config.d.ts +1 -1
- package/dist/lib/config.js +31 -34
- package/dist/lib/constants.d.ts +0 -2
- package/dist/lib/constants.js +20 -27
- package/dist/lib/dev.d.ts +1 -1
- package/dist/lib/dev.js +52 -69
- package/dist/lib/extensionsService.d.ts +1 -1
- package/dist/lib/extensionsService.js +21 -15
- package/dist/lib/parsing-utils.d.ts +1 -1
- package/dist/lib/parsing-utils.js +7 -11
- package/dist/lib/plugins/codeBlockingPlugin.d.ts +1 -1
- package/dist/lib/plugins/codeBlockingPlugin.js +5 -8
- package/dist/lib/plugins/codeCheckingPlugin.d.ts +1 -1
- package/dist/lib/plugins/codeCheckingPlugin.js +4 -9
- package/dist/lib/plugins/devBuildPlugin.d.ts +2 -2
- package/dist/lib/plugins/devBuildPlugin.js +74 -99
- package/dist/lib/plugins/friendlyLoggingPlugin.d.ts +2 -2
- package/dist/lib/plugins/friendlyLoggingPlugin.js +4 -12
- package/dist/lib/plugins/manifestPlugin.d.ts +1 -1
- package/dist/lib/plugins/manifestPlugin.js +46 -26
- package/dist/lib/plugins/relevantModulesPlugin.d.ts +2 -2
- package/dist/lib/plugins/relevantModulesPlugin.js +4 -7
- package/dist/lib/server.d.ts +7 -2
- package/dist/lib/server.js +85 -84
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/types.js +4 -7
- package/dist/lib/utils.d.ts +1 -1
- package/dist/lib/utils.js +22 -39
- package/package.json +44 -31
|
@@ -1,88 +1,70 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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 ===
|
|
7
|
+
if (componentType === UnifiedComponentTypes.SETTINGS) {
|
|
25
8
|
return 'Settings';
|
|
26
9
|
}
|
|
27
|
-
if (componentType ===
|
|
10
|
+
if (componentType === UnifiedComponentTypes.PAGE) {
|
|
28
11
|
return 'App Home';
|
|
29
12
|
}
|
|
30
13
|
}
|
|
31
|
-
class DevModeUnifiedInterface extends
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
59
|
-
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:
|
|
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
|
|
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 = `./${
|
|
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 ===
|
|
80
|
-
extension.componentType ===
|
|
81
|
-
extension.config.name =
|
|
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
|
|
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 =
|
|
94
|
-
const filePath =
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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,
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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 =
|
|
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
|
-
|
|
73
|
-
|
|
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,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
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|