@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.
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/lib/DevModeInterface.d.ts +9 -0
- package/dist/lib/DevModeInterface.js +36 -0
- package/dist/lib/DevModeParentInterface.d.ts +19 -0
- package/dist/lib/DevModeParentInterface.js +181 -0
- package/dist/lib/DevModeUnifiedInterface.d.ts +9 -0
- package/dist/lib/DevModeUnifiedInterface.js +118 -0
- package/dist/lib/DevServerState.d.ts +44 -0
- package/dist/lib/DevServerState.js +95 -0
- 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 +124 -0
- package/dist/lib/__tests__/plugins/devBuildPlugin.spec.d.ts +1 -0
- package/dist/lib/__tests__/plugins/devBuildPlugin.spec.js +396 -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 +115 -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 +16 -0
- package/dist/lib/ast.js +281 -0
- package/dist/lib/bin/cli.d.ts +2 -0
- package/dist/lib/bin/cli.js +143 -0
- package/dist/lib/build.d.ts +24 -0
- package/dist/lib/build.js +73 -0
- package/dist/lib/config.d.ts +7 -0
- package/dist/lib/config.js +124 -0
- package/dist/lib/constants.d.ts +32 -0
- package/dist/lib/constants.js +43 -0
- package/dist/lib/dev.d.ts +2 -0
- package/dist/lib/dev.js +58 -0
- package/dist/lib/extensionsService.d.ts +10 -0
- package/dist/lib/extensionsService.js +45 -0
- package/dist/lib/parsing-utils.d.ts +31 -0
- package/dist/lib/parsing-utils.js +289 -0
- package/dist/lib/plugins/codeBlockingPlugin.d.ts +8 -0
- package/dist/lib/plugins/codeBlockingPlugin.js +45 -0
- package/dist/lib/plugins/codeCheckingPlugin.d.ts +8 -0
- package/dist/lib/plugins/codeCheckingPlugin.js +93 -0
- package/dist/lib/plugins/devBuildPlugin.d.ts +8 -0
- package/dist/lib/plugins/devBuildPlugin.js +212 -0
- package/dist/lib/plugins/friendlyLoggingPlugin.d.ts +14 -0
- package/dist/lib/plugins/friendlyLoggingPlugin.js +36 -0
- package/dist/lib/plugins/manifestPlugin.d.ts +12 -0
- package/dist/lib/plugins/manifestPlugin.js +158 -0
- package/dist/lib/plugins/relevantModulesPlugin.d.ts +14 -0
- package/dist/lib/plugins/relevantModulesPlugin.js +33 -0
- package/dist/lib/server.d.ts +13 -0
- package/dist/lib/server.js +99 -0
- package/dist/lib/types.d.ts +290 -0
- package/dist/lib/types.js +12 -0
- package/dist/lib/utils.d.ts +25 -0
- package/dist/lib/utils.js +113 -0
- package/package.json +2 -1
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { remoteBuild, buildSingleExtension } from './lib/build.ts';
|
|
2
|
+
import DevModeInterface, { DevModeInterfaceNonSingleton } from './lib/DevModeInterface.ts';
|
|
3
|
+
import DevModeUnifiedInterface, { DevModeUnifiedInterfaceNonSingleton } from './lib/DevModeUnifiedInterface.ts';
|
|
4
|
+
export { remoteBuild, buildSingleExtension, DevModeInterface, DevModeInterfaceNonSingleton, DevModeUnifiedInterface, DevModeUnifiedInterfaceNonSingleton, };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { remoteBuild, buildSingleExtension } from "./lib/build.js";
|
|
2
|
+
import DevModeInterface, { DevModeInterfaceNonSingleton, } from "./lib/DevModeInterface.js";
|
|
3
|
+
import DevModeUnifiedInterface, { DevModeUnifiedInterfaceNonSingleton, } from "./lib/DevModeUnifiedInterface.js";
|
|
4
|
+
export { remoteBuild, buildSingleExtension, DevModeInterface, DevModeInterfaceNonSingleton, DevModeUnifiedInterface, DevModeUnifiedInterfaceNonSingleton, };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ProjectComponentMap, AppExtensionMapping, DevModeSetupArguments } from './types.ts';
|
|
2
|
+
import { DevModeParentInterface } from './DevModeParentInterface.ts';
|
|
3
|
+
declare class DevModeInterface extends DevModeParentInterface {
|
|
4
|
+
_generateAppExtensionMappings(components: ProjectComponentMap): AppExtensionMapping[];
|
|
5
|
+
setup(args: DevModeSetupArguments): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export { DevModeInterface as DevModeInterfaceNonSingleton };
|
|
8
|
+
declare const _default: DevModeInterface;
|
|
9
|
+
export default _default;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { SUPPORTED_APP_TYPES, PUBLIC_APP } from "./constants.js";
|
|
2
|
+
import { loadExtensionConfig } from "./config.js";
|
|
3
|
+
import { DevModeParentInterface } from "./DevModeParentInterface.js";
|
|
4
|
+
class DevModeInterface extends DevModeParentInterface {
|
|
5
|
+
_generateAppExtensionMappings(components) {
|
|
6
|
+
// Loop over all of the app configs that are passed in
|
|
7
|
+
const allComponentNames = Object.keys(components);
|
|
8
|
+
return allComponentNames.reduce((appExtensionMappings, componentName) => {
|
|
9
|
+
const component = components[componentName];
|
|
10
|
+
if (!SUPPORTED_APP_TYPES.includes(component.type)) {
|
|
11
|
+
return appExtensionMappings; // It's not a modern app, skip it
|
|
12
|
+
}
|
|
13
|
+
// Load all of the extension configs for a particular app.json file
|
|
14
|
+
const extensionsConfigForApp = loadExtensionConfig(component.config, component.path);
|
|
15
|
+
const extensionConfigKeys = Object.keys(extensionsConfigForApp);
|
|
16
|
+
// Loop over the loaded extension configs and generate the list of choices to use to prompt the user for input
|
|
17
|
+
extensionConfigKeys.forEach((extensionKey) => {
|
|
18
|
+
const extensionConfig = extensionsConfigForApp[extensionKey];
|
|
19
|
+
if (extensionConfig.appConfig) {
|
|
20
|
+
extensionConfig.appConfig.isPublicApp = component.type === PUBLIC_APP;
|
|
21
|
+
}
|
|
22
|
+
appExtensionMappings.push({
|
|
23
|
+
name: `${componentName}/${extensionConfig.data.title}`,
|
|
24
|
+
value: extensionConfig,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
return appExtensionMappings;
|
|
28
|
+
}, []);
|
|
29
|
+
}
|
|
30
|
+
async setup(args) {
|
|
31
|
+
args.choices = this._generateAppExtensionMappings(args.components);
|
|
32
|
+
await this.parentSetup(args);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export { DevModeInterface as DevModeInterfaceNonSingleton };
|
|
36
|
+
export default new DevModeInterface();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { PlatformVersion, ProjectConfig, Logger, ExtensionOutputConfig, ProjectComponentMap, AppExtensionMapping, DevModeStartArguments, DevModeSetupArguments, UnifiedProjectComponentMap, DevModeBaseSetupArguments } from './types.ts';
|
|
2
|
+
import { DevServerState } from './DevServerState.ts';
|
|
3
|
+
export declare abstract class DevModeParentInterface {
|
|
4
|
+
configs?: ExtensionOutputConfig[];
|
|
5
|
+
devServerState?: DevServerState;
|
|
6
|
+
onUploadRequired?: VoidFunction;
|
|
7
|
+
logger: Logger;
|
|
8
|
+
urls?: DevModeSetupArguments['urls'];
|
|
9
|
+
isConfigured?: boolean;
|
|
10
|
+
isRunning?: boolean;
|
|
11
|
+
shutdown?: () => Promise<void>;
|
|
12
|
+
protected abstract _generateAppExtensionMappings(components: ProjectComponentMap | UnifiedProjectComponentMap): AppExtensionMapping[];
|
|
13
|
+
_getPlatformVersion(projectConfig?: ProjectConfig): PlatformVersion;
|
|
14
|
+
_reset(): void;
|
|
15
|
+
parentSetup({ onUploadRequired, logger, urls, setActiveApp, choices, }: DevModeBaseSetupArguments): Promise<void>;
|
|
16
|
+
start({ requestPorts, accountId, projectConfig, }: DevModeStartArguments): Promise<void>;
|
|
17
|
+
fileChange(filePath: string, __event: unknown): Promise<void>;
|
|
18
|
+
cleanup(): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { PLATFORM_VERSION } from "./constants.js";
|
|
2
|
+
import { startDevMode } from "./dev.js";
|
|
3
|
+
import { loadLocalConfig } from "./config.js";
|
|
4
|
+
import { throwUnhandledPlatformVersionError } from "./utils.js";
|
|
5
|
+
import { DevServerState } from "./DevServerState.js";
|
|
6
|
+
import inquirer from 'inquirer';
|
|
7
|
+
import { EXPRESS_DEFAULT_PORT, EXPRESS_SERVER_ID } from "./constants.js";
|
|
8
|
+
const defaultLogger = {
|
|
9
|
+
info: (...args) => {
|
|
10
|
+
console.log(...args);
|
|
11
|
+
},
|
|
12
|
+
debug: (...args) => {
|
|
13
|
+
console.log(...args);
|
|
14
|
+
},
|
|
15
|
+
warn: (...args) => {
|
|
16
|
+
console.error(...args);
|
|
17
|
+
},
|
|
18
|
+
error: (...args) => {
|
|
19
|
+
console.error(...args);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
export class DevModeParentInterface {
|
|
23
|
+
configs;
|
|
24
|
+
devServerState;
|
|
25
|
+
onUploadRequired;
|
|
26
|
+
logger = defaultLogger;
|
|
27
|
+
urls;
|
|
28
|
+
isConfigured;
|
|
29
|
+
isRunning;
|
|
30
|
+
shutdown;
|
|
31
|
+
_getPlatformVersion(projectConfig) {
|
|
32
|
+
const { platformVersion } = projectConfig ?? {};
|
|
33
|
+
if (!platformVersion) {
|
|
34
|
+
return PLATFORM_VERSION.V20231;
|
|
35
|
+
}
|
|
36
|
+
switch (platformVersion) {
|
|
37
|
+
case PLATFORM_VERSION.V20231:
|
|
38
|
+
return PLATFORM_VERSION.V20231;
|
|
39
|
+
case PLATFORM_VERSION.V20232:
|
|
40
|
+
return PLATFORM_VERSION.V20232;
|
|
41
|
+
case PLATFORM_VERSION.V20251:
|
|
42
|
+
return PLATFORM_VERSION.V20251;
|
|
43
|
+
case PLATFORM_VERSION.V20252:
|
|
44
|
+
case PLATFORM_VERSION.UNSTABLE:
|
|
45
|
+
return PLATFORM_VERSION.V20252;
|
|
46
|
+
default:
|
|
47
|
+
return throwUnhandledPlatformVersionError(platformVersion);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
_reset() {
|
|
51
|
+
this.configs = undefined;
|
|
52
|
+
this.devServerState = undefined;
|
|
53
|
+
this.onUploadRequired = undefined;
|
|
54
|
+
this.shutdown = undefined;
|
|
55
|
+
this.logger = defaultLogger;
|
|
56
|
+
this.urls = undefined;
|
|
57
|
+
this.isConfigured = false;
|
|
58
|
+
this.isRunning = false;
|
|
59
|
+
}
|
|
60
|
+
async parentSetup({ onUploadRequired, logger, urls, setActiveApp, choices = [], }) {
|
|
61
|
+
if (this.isConfigured) {
|
|
62
|
+
logger.debug('Dev server has already been configured, skipping');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.logger = logger;
|
|
66
|
+
this.onUploadRequired = onUploadRequired;
|
|
67
|
+
this.urls = urls;
|
|
68
|
+
if (choices.length === 0) {
|
|
69
|
+
throw new Error('No extensions to run');
|
|
70
|
+
}
|
|
71
|
+
else if (choices.length === 1) {
|
|
72
|
+
this.configs = [choices[0].value];
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
const promptModule = inquirer.createPromptModule();
|
|
76
|
+
const answers = await promptModule({
|
|
77
|
+
type: 'checkbox',
|
|
78
|
+
name: 'extensions',
|
|
79
|
+
message: 'Which extension(s) would you like to run?',
|
|
80
|
+
validate: (selectedChoices) => {
|
|
81
|
+
if (!selectedChoices || selectedChoices.length === 0) {
|
|
82
|
+
return 'Select at least one extension to run';
|
|
83
|
+
}
|
|
84
|
+
const configs = selectedChoices
|
|
85
|
+
.map((choice) => choice.value)
|
|
86
|
+
.filter((value) => {
|
|
87
|
+
return (typeof value === 'object' && value !== null && 'data' in value);
|
|
88
|
+
});
|
|
89
|
+
if (configs.length !== selectedChoices.length) {
|
|
90
|
+
return 'Invalid extension configuration';
|
|
91
|
+
}
|
|
92
|
+
const appNames = new Set(configs.map((config) => config.data.appName));
|
|
93
|
+
if (appNames.size > 1) {
|
|
94
|
+
return 'Running multiple extensions is only supported for a single application';
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
},
|
|
98
|
+
choices,
|
|
99
|
+
});
|
|
100
|
+
this.configs = answers.extensions;
|
|
101
|
+
}
|
|
102
|
+
this.isConfigured = true;
|
|
103
|
+
if (typeof setActiveApp === 'function') {
|
|
104
|
+
await setActiveApp(this.configs?.[0]?.appConfig?.uid);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async start({ requestPorts, accountId, projectConfig, }) {
|
|
108
|
+
this.logger.debug('Start function was invoked', {
|
|
109
|
+
accountId,
|
|
110
|
+
projectConfig,
|
|
111
|
+
});
|
|
112
|
+
if (this.isRunning) {
|
|
113
|
+
this.logger.debug('Dev server is already running, not starting again');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
let expressPort = EXPRESS_DEFAULT_PORT;
|
|
117
|
+
if (requestPorts) {
|
|
118
|
+
try {
|
|
119
|
+
const portData = await requestPorts([
|
|
120
|
+
{ instanceId: EXPRESS_SERVER_ID, port: EXPRESS_DEFAULT_PORT },
|
|
121
|
+
]);
|
|
122
|
+
expressPort = portData[EXPRESS_SERVER_ID];
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
if (e?.status === 409) {
|
|
126
|
+
throw new Error('Another instance is already running. To proceed, please stop the existing server and try again.');
|
|
127
|
+
}
|
|
128
|
+
this.logger.debug('Call to port manager failed, using default ports');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const { proxy: localDevUrlMapping } = loadLocalConfig(this.configs?.[0]?.path || '', this.logger) || {};
|
|
132
|
+
try {
|
|
133
|
+
this.devServerState = new DevServerState({
|
|
134
|
+
localDevUrlMapping,
|
|
135
|
+
extensionConfigs: this.configs,
|
|
136
|
+
accountId,
|
|
137
|
+
platformVersion: this._getPlatformVersion(projectConfig),
|
|
138
|
+
expressPort,
|
|
139
|
+
logger: this.logger,
|
|
140
|
+
urls: this.urls,
|
|
141
|
+
appConfig: this.configs?.[0]?.appConfig,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
this.logger.debug('Error setting up DevServerState', e);
|
|
146
|
+
throw e;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
this.shutdown = await startDevMode(this.devServerState);
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
this.logger.debug('Error starting dev mode', e);
|
|
153
|
+
throw e;
|
|
154
|
+
}
|
|
155
|
+
const { extensionsMetadata } = this.devServerState;
|
|
156
|
+
extensionsMetadata.forEach((metadata) => {
|
|
157
|
+
const { config: { data: { title, appName }, }, } = metadata;
|
|
158
|
+
this.logger.info(`Running extension '${title}' from app '${appName}'`);
|
|
159
|
+
});
|
|
160
|
+
this.isRunning = true;
|
|
161
|
+
}
|
|
162
|
+
// The contract is for this to be async, with the __event param. Eslint doesn't like it.
|
|
163
|
+
// eslint-disable-next-line require-await
|
|
164
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
165
|
+
async fileChange(filePath, __event) {
|
|
166
|
+
if (!this.devServerState || !this.devServerState.extensionsMetadata) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const relevantConfigFileChanged = this.devServerState.extensionsMetadata.some((metadata) => metadata.config.extensionConfigPath === filePath);
|
|
170
|
+
if (relevantConfigFileChanged && this.onUploadRequired) {
|
|
171
|
+
this.onUploadRequired();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async cleanup() {
|
|
175
|
+
if (this.shutdown) {
|
|
176
|
+
await this.shutdown();
|
|
177
|
+
}
|
|
178
|
+
// Since the DevModeInterface is a singleton, we need to wipe out the state when we shutdown
|
|
179
|
+
this._reset();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { AppExtensionMapping, UnifiedProjectComponentMap, UnifiedDevModeSetupArguments, ProfileVariables } from './types.ts';
|
|
2
|
+
import { DevModeParentInterface } from './DevModeParentInterface.ts';
|
|
3
|
+
declare class DevModeUnifiedInterface extends DevModeParentInterface {
|
|
4
|
+
_generateAppExtensionMappings(components: UnifiedProjectComponentMap, profileData?: ProfileVariables): AppExtensionMapping[];
|
|
5
|
+
setup(args: UnifiedDevModeSetupArguments): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export { DevModeUnifiedInterface as DevModeUnifiedInterfaceNonSingleton };
|
|
8
|
+
declare const _default: DevModeUnifiedInterface;
|
|
9
|
+
export default _default;
|
|
@@ -0,0 +1,118 @@
|
|
|
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";
|
|
6
|
+
function getComponentName(componentType) {
|
|
7
|
+
if (componentType === UnifiedComponentTypes.SETTINGS) {
|
|
8
|
+
return 'Settings';
|
|
9
|
+
}
|
|
10
|
+
if (componentType === UnifiedComponentTypes.PAGE) {
|
|
11
|
+
return 'App Home';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
class DevModeUnifiedInterface extends DevModeParentInterface {
|
|
15
|
+
_generateAppExtensionMappings(components, profileData) {
|
|
16
|
+
const mappings = [];
|
|
17
|
+
// Loop over all of the components that are passed in
|
|
18
|
+
const allComponentUids = Object.keys(components);
|
|
19
|
+
// Find the app component
|
|
20
|
+
const appUid = allComponentUids.find((componentUid) => {
|
|
21
|
+
return (components[componentUid].componentType ===
|
|
22
|
+
UnifiedComponentTypes.APPLICATION);
|
|
23
|
+
});
|
|
24
|
+
// This should fail a lot sooner (on the cli side), but added this just in case.
|
|
25
|
+
if (!appUid) {
|
|
26
|
+
// TODO: Add this once logger is set up on parent class
|
|
27
|
+
// this.logger.error('Application configuration is missing.');
|
|
28
|
+
return mappings;
|
|
29
|
+
}
|
|
30
|
+
const appData = components[appUid];
|
|
31
|
+
// Use the app data to generate the app config in the expected shape, to match old projects.
|
|
32
|
+
const appConfig = {
|
|
33
|
+
name: appData.config.name,
|
|
34
|
+
description: appData.config?.description,
|
|
35
|
+
uid: appData.uid,
|
|
36
|
+
extensions: {
|
|
37
|
+
crm: {
|
|
38
|
+
cards: [],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
auth: appData.config?.auth,
|
|
42
|
+
support: appData.config?.support,
|
|
43
|
+
// All unified apps are currently "public" meaning they don't support serverless and do support a proxy.
|
|
44
|
+
// The determination of what is "public" and "private" as it relates to our usage is still up in the air.
|
|
45
|
+
isPublicApp: true,
|
|
46
|
+
allowedUrls: appData.config?.permittedUrls
|
|
47
|
+
? Object.values(appData.config.permittedUrls).flat()
|
|
48
|
+
: [],
|
|
49
|
+
variables: profileData || {},
|
|
50
|
+
};
|
|
51
|
+
// Then get all the supported extensions
|
|
52
|
+
const extensionUids = allComponentUids.filter((componentUid) => {
|
|
53
|
+
return SUPPORTED_EXTENSION_TYPES.includes(components[componentUid].componentType);
|
|
54
|
+
});
|
|
55
|
+
// Build the extension mapping data
|
|
56
|
+
extensionUids.forEach((extensionUid) => {
|
|
57
|
+
const extension = components[extensionUid];
|
|
58
|
+
// Update the extension entrypoint to be "relative" to the extension directory (eg from /app/card/card.jsx to ./card.jsx)
|
|
59
|
+
extension.config.entrypoint = `./${path.basename(extension.config.entrypoint)}`;
|
|
60
|
+
// Hardcode the extension name if this is a settings extension (the user does not provide a name for their settings card).
|
|
61
|
+
if (extension.componentType === UnifiedComponentTypes.SETTINGS ||
|
|
62
|
+
extension.componentType === UnifiedComponentTypes.PAGE) {
|
|
63
|
+
extension.config.name = getComponentName(extension.componentType) ?? '';
|
|
64
|
+
}
|
|
65
|
+
// Add them to the app config
|
|
66
|
+
switch (extension.componentType) {
|
|
67
|
+
case UnifiedComponentTypes.CARD:
|
|
68
|
+
default:
|
|
69
|
+
appConfig.extensions.crm.cards.push({
|
|
70
|
+
file: extension.config.entrypoint,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// Generate the name and other extension data for the mapping.
|
|
74
|
+
const extensionName = `${appData.config.name}/${extension.config.name}`;
|
|
75
|
+
const extensionOutput = getUrlSafeFileName(path.resolve(extension.localDev.componentRoot, extension.config.entrypoint));
|
|
76
|
+
const filePath = path.resolve(extension.localDev.componentRoot, extension.config.entrypoint);
|
|
77
|
+
// Build the config in the correct shape
|
|
78
|
+
const extensionData = {
|
|
79
|
+
title: extension.config.name,
|
|
80
|
+
uid: extension.uid,
|
|
81
|
+
location: extension.config.location,
|
|
82
|
+
module: {
|
|
83
|
+
file: filePath,
|
|
84
|
+
},
|
|
85
|
+
objectTypes: [],
|
|
86
|
+
appName: appData.config.name,
|
|
87
|
+
sourceId: `${appConfig.uid}::${extension.uid}`,
|
|
88
|
+
};
|
|
89
|
+
// Generate object types
|
|
90
|
+
if (extension.config.objectTypes) {
|
|
91
|
+
extension.config.objectTypes.forEach((objectType) => {
|
|
92
|
+
extensionData.objectTypes.push({
|
|
93
|
+
name: objectType,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
mappings.push({
|
|
98
|
+
name: extensionName,
|
|
99
|
+
value: {
|
|
100
|
+
type: extension.componentType.toLowerCase(),
|
|
101
|
+
data: extensionData,
|
|
102
|
+
output: extensionOutput,
|
|
103
|
+
path: appData.localDev.componentRoot,
|
|
104
|
+
extensionPath: extension.localDev.componentRoot,
|
|
105
|
+
extensionConfigPath: extension.localDev.componentConfigPath,
|
|
106
|
+
appConfig,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
return mappings;
|
|
111
|
+
}
|
|
112
|
+
async setup(args) {
|
|
113
|
+
args.choices = this._generateAppExtensionMappings(args.components, args.profileData ?? {});
|
|
114
|
+
await this.parentSetup(args);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export { DevModeUnifiedInterface as DevModeUnifiedInterfaceNonSingleton };
|
|
118
|
+
export default new DevModeUnifiedInterface();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { AppConfig, ExtensionOutputConfig, ExtensionMetadata, Logger, PlatformVersion } from './types.ts';
|
|
2
|
+
import { ServiceConfiguration, ProxyServiceConfig } from '@hubspot/app-functions-dev-server';
|
|
3
|
+
import { ExtensionsWebSocket } from './ExtensionsWebSocket.ts';
|
|
4
|
+
type DevServerStateLocalDevUrlMapping = ProxyServiceConfig['localDevUrlMapping'] | undefined;
|
|
5
|
+
interface DevServerStateArgs {
|
|
6
|
+
localDevUrlMapping?: DevServerStateLocalDevUrlMapping;
|
|
7
|
+
extensionConfigs?: ExtensionOutputConfig[];
|
|
8
|
+
accountId: number | undefined;
|
|
9
|
+
expressPort: number;
|
|
10
|
+
platformVersion: PlatformVersion;
|
|
11
|
+
logger: Logger;
|
|
12
|
+
urls: {
|
|
13
|
+
api: string;
|
|
14
|
+
web: string;
|
|
15
|
+
};
|
|
16
|
+
appConfig?: AppConfig;
|
|
17
|
+
}
|
|
18
|
+
export declare class DevServerState {
|
|
19
|
+
private _expressPort;
|
|
20
|
+
private _functionsConfig;
|
|
21
|
+
private _localDevUrlMapping;
|
|
22
|
+
private _outputDir;
|
|
23
|
+
private _appPath;
|
|
24
|
+
private _extensionsMetadata;
|
|
25
|
+
private _portalId?;
|
|
26
|
+
private _mutableState;
|
|
27
|
+
logger: Logger;
|
|
28
|
+
appConfig?: AppConfig;
|
|
29
|
+
constructor({ localDevUrlMapping, extensionConfigs, accountId, expressPort, platformVersion, logger, urls, appConfig, }: DevServerStateArgs);
|
|
30
|
+
get portalId(): number | undefined;
|
|
31
|
+
get expressPort(): number;
|
|
32
|
+
get extensionsMetadata(): ExtensionMetadata[];
|
|
33
|
+
get functionsConfig(): Partial<ServiceConfiguration>;
|
|
34
|
+
get outputDir(): string;
|
|
35
|
+
get appPath(): string;
|
|
36
|
+
get localDevUrlMapping(): DevServerStateLocalDevUrlMapping;
|
|
37
|
+
get extensionsWebSocket(): ExtensionsWebSocket | undefined;
|
|
38
|
+
set extensionsWebSocket(ws: ExtensionsWebSocket);
|
|
39
|
+
getExtensionsWebSocket(): ExtensionsWebSocket;
|
|
40
|
+
isPublicApp(): boolean;
|
|
41
|
+
setWebSocketSetupCallback(callback: () => void): void;
|
|
42
|
+
triggerWebSocketSetup(): void;
|
|
43
|
+
}
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
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, }) {
|
|
15
|
+
if (!extensionConfigs) {
|
|
16
|
+
throw new Error('Unable to load the required extension configuration files');
|
|
17
|
+
}
|
|
18
|
+
const extensionsMetadata = [];
|
|
19
|
+
extensionConfigs.forEach((config) => {
|
|
20
|
+
const { appName, title, sourceId } = config.data;
|
|
21
|
+
extensionsMetadata.push({
|
|
22
|
+
config,
|
|
23
|
+
baseMessage: {
|
|
24
|
+
appName,
|
|
25
|
+
title,
|
|
26
|
+
sourceId,
|
|
27
|
+
callback: `http://hslocal.net:${expressPort}/${config.output}`,
|
|
28
|
+
portalId: accountId,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
this._expressPort = expressPort;
|
|
33
|
+
this._extensionsMetadata = extensionsMetadata;
|
|
34
|
+
this._appPath = extensionConfigs[0].path;
|
|
35
|
+
this._portalId = accountId;
|
|
36
|
+
this._outputDir = path.join(this._appPath, OUTPUT_DIR);
|
|
37
|
+
// Pass options from the CLI for running app functions locally
|
|
38
|
+
this._functionsConfig = {
|
|
39
|
+
app: { path: this._appPath },
|
|
40
|
+
accountId,
|
|
41
|
+
platformVersion,
|
|
42
|
+
hubspotApiOrigin: urls.api,
|
|
43
|
+
hubspotWebsiteOrigin: urls.web,
|
|
44
|
+
};
|
|
45
|
+
this._localDevUrlMapping = localDevUrlMapping;
|
|
46
|
+
this.logger = logger;
|
|
47
|
+
this.appConfig = appConfig;
|
|
48
|
+
Object.freeze(this);
|
|
49
|
+
}
|
|
50
|
+
get portalId() {
|
|
51
|
+
return this._portalId;
|
|
52
|
+
}
|
|
53
|
+
get expressPort() {
|
|
54
|
+
return this._expressPort;
|
|
55
|
+
}
|
|
56
|
+
get extensionsMetadata() {
|
|
57
|
+
return this._extensionsMetadata;
|
|
58
|
+
}
|
|
59
|
+
get functionsConfig() {
|
|
60
|
+
return this._functionsConfig;
|
|
61
|
+
}
|
|
62
|
+
get outputDir() {
|
|
63
|
+
return this._outputDir;
|
|
64
|
+
}
|
|
65
|
+
get appPath() {
|
|
66
|
+
return this._appPath;
|
|
67
|
+
}
|
|
68
|
+
get localDevUrlMapping() {
|
|
69
|
+
return this._localDevUrlMapping;
|
|
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
|
+
}
|
|
83
|
+
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
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -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
|
+
}
|