@hubspot/ui-extensions-dev-server 1.1.0 → 1.1.1
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 +111 -0
- package/dist/lib/__tests__/plugins/devBuildPlugin.spec.d.ts +1 -0
- package/dist/lib/__tests__/plugins/devBuildPlugin.spec.js +345 -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 +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 +89 -0
- package/dist/lib/plugins/devBuildPlugin.d.ts +8 -0
- package/dist/lib/plugins/devBuildPlugin.js +201 -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 +13 -0
- package/dist/lib/plugins/relevantModulesPlugin.js +25 -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 +1 -1
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts the value from a given AST node based on its type.
|
|
3
|
+
* This function handles various node types such as Literal, Identifier (aka variables), ArrayExpression,
|
|
4
|
+
* ObjectExpression, SpreadElement, TemplateLiteral, UnaryExpression, and MemberExpression (accessing props on objects).
|
|
5
|
+
* It also supports nested structures and handles special cases like undefined, NaN, and Infinity.
|
|
6
|
+
*
|
|
7
|
+
* It does not handle function calls, complex expressions, or anything that requires evaluation of code logic.
|
|
8
|
+
* This is not a full JavaScript interpreter, but rather a utility to extract static values from the AST.
|
|
9
|
+
*
|
|
10
|
+
* Use this function carefully, as it assumes that the AST is well-formed and that the node types are as expected.
|
|
11
|
+
* Many types are not yet supported. Anything this function is used for should have a backup runtime evaluation,
|
|
12
|
+
* or be set up to fail gracefully if parsing fails.
|
|
13
|
+
*
|
|
14
|
+
* @param node - The AST node from which to extract the value.
|
|
15
|
+
* @param state - The current state of the source code metadata, including variable declarations.
|
|
16
|
+
* @returns An object containing the status of the operation, the extracted node value,
|
|
17
|
+
* or an error message if the extraction fails.
|
|
18
|
+
* The status can be 'SUCCESS' or 'FAIL'.
|
|
19
|
+
* If the status is 'SUCCESS', nodeValue will contain the extracted value.
|
|
20
|
+
* If the status is 'FAIL', error will contain the error message.
|
|
21
|
+
*/
|
|
22
|
+
export function getValueFromNode(node, state) {
|
|
23
|
+
try {
|
|
24
|
+
switch (node.type) {
|
|
25
|
+
case 'Literal':
|
|
26
|
+
return { status: 'SUCCESS', nodeValue: node.value };
|
|
27
|
+
case 'Identifier': {
|
|
28
|
+
const name = node.name;
|
|
29
|
+
switch (name) {
|
|
30
|
+
case 'undefined':
|
|
31
|
+
return { status: 'SUCCESS', nodeValue: undefined };
|
|
32
|
+
case 'NaN':
|
|
33
|
+
return { status: 'SUCCESS', nodeValue: NaN };
|
|
34
|
+
case 'Infinity':
|
|
35
|
+
return { status: 'SUCCESS', nodeValue: Infinity };
|
|
36
|
+
default:
|
|
37
|
+
if (state.variableDeclarations.has(name)) {
|
|
38
|
+
return {
|
|
39
|
+
status: 'SUCCESS',
|
|
40
|
+
nodeValue: state.variableDeclarations.get(name),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// If for some reason the variable tracking fails, return unsupported.
|
|
44
|
+
return { status: 'FAIL', error: `Identifier ${name} is not found` };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
case 'ArrayExpression': {
|
|
48
|
+
const arrayValue = [];
|
|
49
|
+
if (node.elements.length === 0) {
|
|
50
|
+
return { status: 'SUCCESS', nodeValue: arrayValue };
|
|
51
|
+
}
|
|
52
|
+
// Arrays have to be built from their elements, to handle special cases like nested arrays from spread operators.
|
|
53
|
+
for (const element of node.elements) {
|
|
54
|
+
if (typeof element === 'object' && element !== null) {
|
|
55
|
+
if (element.type === 'SpreadElement') {
|
|
56
|
+
const result = getValueFromNode(element, state);
|
|
57
|
+
if (result.status === 'FAIL') {
|
|
58
|
+
return {
|
|
59
|
+
status: 'FAIL',
|
|
60
|
+
error: `Array spread element failed: ${result.error}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const value = result.nodeValue;
|
|
64
|
+
if (Array.isArray(value)) {
|
|
65
|
+
arrayValue.push(...value);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
arrayValue.push(value);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const result = getValueFromNode(element, state);
|
|
73
|
+
if (result.status === 'FAIL') {
|
|
74
|
+
return {
|
|
75
|
+
status: 'FAIL',
|
|
76
|
+
error: `Array element failed: ${result.error}`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
arrayValue.push(result.nodeValue);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
arrayValue.push(element);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { status: 'SUCCESS', nodeValue: arrayValue };
|
|
87
|
+
}
|
|
88
|
+
case 'ObjectExpression': {
|
|
89
|
+
const obj = {};
|
|
90
|
+
for (const prop of node.properties) {
|
|
91
|
+
switch (prop.type) {
|
|
92
|
+
case 'Property': {
|
|
93
|
+
const property = prop;
|
|
94
|
+
let key = undefined;
|
|
95
|
+
if (property.key.type === 'Identifier') {
|
|
96
|
+
key = property.key.name;
|
|
97
|
+
}
|
|
98
|
+
else if (property.key.type === 'Literal') {
|
|
99
|
+
key = String(property.key.value);
|
|
100
|
+
}
|
|
101
|
+
if (key) {
|
|
102
|
+
const result = getValueFromNode(property.value, state);
|
|
103
|
+
if (result.status === 'FAIL') {
|
|
104
|
+
return {
|
|
105
|
+
status: 'FAIL',
|
|
106
|
+
error: `Object property '${key}' failed: ${result.error}`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
obj[key] = result.nodeValue;
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case 'SpreadElement': {
|
|
114
|
+
const result = getValueFromNode(prop, state);
|
|
115
|
+
if (result.status === 'FAIL') {
|
|
116
|
+
return {
|
|
117
|
+
status: 'FAIL',
|
|
118
|
+
error: `Object spread element failed: ${result.error}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const spreadValue = result.nodeValue;
|
|
122
|
+
if (spreadValue && typeof spreadValue === 'object') {
|
|
123
|
+
Object.assign(obj, spreadValue);
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
default:
|
|
128
|
+
// Ignore unsupported property types, as we don't have a key for them.
|
|
129
|
+
// This could be a computed property or something else we don't handle.
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { status: 'SUCCESS', nodeValue: obj };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Spread elements are a bit tricky. They can be used to directly spread an array or object,
|
|
137
|
+
* or they can be used to spread a variable that is defined elsewhere. Our strategy is to return
|
|
138
|
+
* whatever element should be spread, and then handle the spreading in the parent array or object.
|
|
139
|
+
*
|
|
140
|
+
* There are also trickier cases we don't handle, like spreading a function call that returns an
|
|
141
|
+
* array or object. When the spread element is unsupported, we return a special symbol.
|
|
142
|
+
*/
|
|
143
|
+
case 'SpreadElement':
|
|
144
|
+
if (node.argument) {
|
|
145
|
+
if (node.argument.type === 'Identifier' && node.argument.name) {
|
|
146
|
+
return {
|
|
147
|
+
status: 'SUCCESS',
|
|
148
|
+
nodeValue: state.variableDeclarations.get(node.argument.name) || null,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
else if (node.argument.type === 'ArrayExpression' ||
|
|
152
|
+
node.argument.type === 'ObjectExpression') {
|
|
153
|
+
return getValueFromNode(node.argument, state);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
status: 'FAIL',
|
|
158
|
+
error: `Unsupported SpreadElement type: ${node.argument.type}`,
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Template literals are built of an alternating sequence of static
|
|
162
|
+
* strings and expressions. We will concatenate the static strings and
|
|
163
|
+
* evaluate the expressions to build the final string.
|
|
164
|
+
*/
|
|
165
|
+
case 'TemplateLiteral': {
|
|
166
|
+
let result = '';
|
|
167
|
+
if (node.expressions && node.quasis) {
|
|
168
|
+
const { quasis, expressions } = node;
|
|
169
|
+
for (let i = 0; i < quasis.length; i++) {
|
|
170
|
+
// Prefer cooked value if available, otherwise use raw.
|
|
171
|
+
result += quasis[i].value.cooked || quasis[i].value.raw;
|
|
172
|
+
if (i < expressions.length) {
|
|
173
|
+
const expression = expressions[i];
|
|
174
|
+
const expressionResult = getValueFromNode(expression, state);
|
|
175
|
+
if (expressionResult.status === 'SUCCESS') {
|
|
176
|
+
const expressionValue = expressionResult.nodeValue;
|
|
177
|
+
if (expressionValue === null ||
|
|
178
|
+
expressionValue === undefined ||
|
|
179
|
+
typeof expressionValue === 'string' ||
|
|
180
|
+
typeof expressionValue === 'number' ||
|
|
181
|
+
typeof expressionValue === 'boolean') {
|
|
182
|
+
result += String(expressionValue);
|
|
183
|
+
}
|
|
184
|
+
else if (Array.isArray(expressionValue)) {
|
|
185
|
+
result += expressionValue.join(',');
|
|
186
|
+
}
|
|
187
|
+
else if (typeof expressionValue === 'object') {
|
|
188
|
+
result += '[object Object]';
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
result += `Unsupported TemplateLiteral expression value type: ${typeof expressionValue}`;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
result += expressionResult.error;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { status: 'SUCCESS', nodeValue: result };
|
|
201
|
+
}
|
|
202
|
+
case 'UnaryExpression': {
|
|
203
|
+
const argResult = getValueFromNode(node.argument, state);
|
|
204
|
+
if (argResult.status === 'FAIL') {
|
|
205
|
+
return argResult;
|
|
206
|
+
}
|
|
207
|
+
const arg = argResult.nodeValue;
|
|
208
|
+
switch (node.operator) {
|
|
209
|
+
case '-':
|
|
210
|
+
return {
|
|
211
|
+
status: 'SUCCESS',
|
|
212
|
+
nodeValue: typeof arg === 'number' ? -arg : NaN,
|
|
213
|
+
};
|
|
214
|
+
case '+':
|
|
215
|
+
return {
|
|
216
|
+
status: 'SUCCESS',
|
|
217
|
+
nodeValue: typeof arg === 'number' ? +arg : NaN,
|
|
218
|
+
};
|
|
219
|
+
case '!':
|
|
220
|
+
return { status: 'SUCCESS', nodeValue: !arg };
|
|
221
|
+
case 'typeof':
|
|
222
|
+
return { status: 'SUCCESS', nodeValue: typeof arg };
|
|
223
|
+
default:
|
|
224
|
+
return {
|
|
225
|
+
status: 'FAIL',
|
|
226
|
+
error: `Unsupported unary operator: ${node.operator}`,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Member expressions are used to access properties of objects.
|
|
231
|
+
case 'MemberExpression': {
|
|
232
|
+
if (node.property.type === 'Identifier' && node.property.name) {
|
|
233
|
+
// We have to recursively get the value of the object, due to the way that nested objects are parsed.
|
|
234
|
+
const objectResult = getValueFromNode(node.object, state);
|
|
235
|
+
if (objectResult.status === 'FAIL') {
|
|
236
|
+
return objectResult;
|
|
237
|
+
}
|
|
238
|
+
const objectData = objectResult.nodeValue;
|
|
239
|
+
if (objectData &&
|
|
240
|
+
typeof objectData === 'object' &&
|
|
241
|
+
!Array.isArray(objectData) &&
|
|
242
|
+
!(objectData instanceof RegExp)) {
|
|
243
|
+
return {
|
|
244
|
+
status: 'SUCCESS',
|
|
245
|
+
nodeValue: objectData[node.property.name],
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
status: 'FAIL',
|
|
251
|
+
error: `Unsupported MemberExpression property type: ${node.property.type}`,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
default:
|
|
255
|
+
return { status: 'FAIL', error: `Unsupported node type: ${node.type}` };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
return {
|
|
260
|
+
status: 'FAIL',
|
|
261
|
+
error: error instanceof Error ? error.message : String(error),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
export function isVariableImported(node, variableName) {
|
|
266
|
+
if (!node) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
return ((node.type === 'ImportSpecifier' &&
|
|
270
|
+
node.imported.type === 'Identifier' &&
|
|
271
|
+
node.imported.name === variableName) ||
|
|
272
|
+
(node.type === 'ImportDefaultSpecifier' &&
|
|
273
|
+
node.local.name === variableName) ||
|
|
274
|
+
(node.type === 'ImportNamespaceSpecifier' &&
|
|
275
|
+
node.local.name === variableName));
|
|
276
|
+
}
|
|
277
|
+
export function isIdentifierDefined(node, parent, name) {
|
|
278
|
+
if (parent &&
|
|
279
|
+
(parent.type === 'MemberExpression' || parent.type === 'CallExpression')) {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
return node.type === 'Identifier' && node.name === name;
|
|
283
|
+
}
|
|
284
|
+
export function isFunctionInvoked(node, functionName) {
|
|
285
|
+
return (node.type === 'CallExpression' &&
|
|
286
|
+
node.callee &&
|
|
287
|
+
'name' in node.callee &&
|
|
288
|
+
node.callee.name === functionName);
|
|
289
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Rollup } from 'vite';
|
|
2
|
+
import { Logger } from '../types.ts';
|
|
3
|
+
export type CodeBlockingPlugin = ({ logger, extensionPath, }: {
|
|
4
|
+
logger: Logger;
|
|
5
|
+
extensionPath: string;
|
|
6
|
+
}) => Rollup.Plugin;
|
|
7
|
+
declare const codeBlockingPlugin: CodeBlockingPlugin;
|
|
8
|
+
export default codeBlockingPlugin;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { isNodeModule } from "../utils.js";
|
|
2
|
+
import { traverseAbstractSyntaxTree } from "../ast.js";
|
|
3
|
+
const codeBlockingPlugin = ({ logger, extensionPath }) => {
|
|
4
|
+
return {
|
|
5
|
+
name: 'ui-extensions-code-blocking-plugin',
|
|
6
|
+
enforce: 'post', // run after default rollup plugins
|
|
7
|
+
transform(code, filename) {
|
|
8
|
+
if (isNodeModule(filename)) {
|
|
9
|
+
return { code, map: null }; // We don't want to parse node modules
|
|
10
|
+
}
|
|
11
|
+
let sourceCodeMetadata = {
|
|
12
|
+
functions: {},
|
|
13
|
+
badImports: [],
|
|
14
|
+
dataDependencies: {
|
|
15
|
+
importedHooks: {},
|
|
16
|
+
dependencies: [],
|
|
17
|
+
},
|
|
18
|
+
variableDeclarations: new Map(),
|
|
19
|
+
};
|
|
20
|
+
const requireFunctionName = 'require';
|
|
21
|
+
try {
|
|
22
|
+
// Not sure why the types don't match for this.parse and the Rollup docs, but
|
|
23
|
+
// the docs over on rollup's site specify ESTree.Program as the return type,
|
|
24
|
+
// and the underlying data matches that https://rollupjs.org/plugin-development/#this-parse
|
|
25
|
+
const abstractSyntaxTree = this.parse(code);
|
|
26
|
+
sourceCodeMetadata = traverseAbstractSyntaxTree(abstractSyntaxTree, [{ functionName: requireFunctionName }], extensionPath, logger);
|
|
27
|
+
if (sourceCodeMetadata.badImports) {
|
|
28
|
+
for (const badImport of sourceCodeMetadata.badImports) {
|
|
29
|
+
logger.warn(`Importing files from outside of the extension directory is not supported. Please move the import ${badImport} into the extension directory.`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
logger.debug('Unable to parse and traverse source code');
|
|
35
|
+
return { code, map: null };
|
|
36
|
+
}
|
|
37
|
+
if (sourceCodeMetadata.functions[requireFunctionName] &&
|
|
38
|
+
sourceCodeMetadata.functions[requireFunctionName].scope === 'Global') {
|
|
39
|
+
logger.warn('require statements are not supported, replace require statements with import');
|
|
40
|
+
}
|
|
41
|
+
return { code, map: null };
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
export default codeBlockingPlugin;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Rollup } from 'vite';
|
|
2
|
+
import { Logger } from '../types.ts';
|
|
3
|
+
export interface CodeCheckingPluginOptions {
|
|
4
|
+
logger: Logger;
|
|
5
|
+
}
|
|
6
|
+
export type CodeCheckingPlugin = (options: CodeCheckingPluginOptions) => Rollup.Plugin;
|
|
7
|
+
declare const codeCheckingPlugin: CodeCheckingPlugin;
|
|
8
|
+
export default codeCheckingPlugin;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
// Matches import statements from @hubspot/ui-extensions that include "hubspot" in the import list.
|
|
3
|
+
// Example matches: "import { hubspot } from '@hubspot/ui-extensions'"
|
|
4
|
+
// "import { hubspot, other } from '@hubspot/ui-extensions'"
|
|
5
|
+
// "import { some, hubspot, more } from \"@hubspot/ui-extensions\""
|
|
6
|
+
const HUBSPOT_IMPORT_REGEX = /import\s*\{[^}]*\bhubspot\b[^}]*\}\s*from\s*['"]@hubspot\/ui-extensions['"]/;
|
|
7
|
+
// Matches namespace import statements from @hubspot/ui-extensions.
|
|
8
|
+
// Example matches: "import * as hubspot from '@hubspot/ui-extensions'"
|
|
9
|
+
// "import * as uie from '@hubspot/ui-extensions'"
|
|
10
|
+
const NAMESPACE_IMPORT_REGEX = /import\s+\*\s+as\s+\w+\s+from\s+['"]@hubspot\/ui-extensions['"]/;
|
|
11
|
+
const codeCheckingPlugin = (options) => {
|
|
12
|
+
const { logger } = options;
|
|
13
|
+
let foundHubspotImport = false;
|
|
14
|
+
return {
|
|
15
|
+
name: 'ui-extensions-code-checking-plugin',
|
|
16
|
+
/**
|
|
17
|
+
* Called by Rollup once at the beginning of each build, before any modules are processed.
|
|
18
|
+
* This hook is invoked when a new build starts, allowing plugins to reset state or
|
|
19
|
+
* perform initialization tasks. In watch mode, this is called each time a rebuild is triggered.
|
|
20
|
+
*
|
|
21
|
+
* We reset the `foundHubspotImport` flag here to ensure we check for the import
|
|
22
|
+
* in each new build cycle.
|
|
23
|
+
*/
|
|
24
|
+
buildStart() {
|
|
25
|
+
foundHubspotImport = false;
|
|
26
|
+
},
|
|
27
|
+
/**
|
|
28
|
+
* Called by Rollup for each module during the build process, after the module's source code
|
|
29
|
+
* has been loaded but before it's parsed and analyzed. This hook allows plugins to transform
|
|
30
|
+
* the source code or perform analysis on it.
|
|
31
|
+
*
|
|
32
|
+
* The `code` parameter contains the raw source code of the module being processed.
|
|
33
|
+
* This hook is called for every module that Rollup processes, including entry points
|
|
34
|
+
* and all their dependencies.
|
|
35
|
+
*
|
|
36
|
+
* We use this hook to scan each module's source code for the required `hubspot` import
|
|
37
|
+
* from `@hubspot/ui-extensions`. Once we find it, we set a flag to avoid unnecessary
|
|
38
|
+
* regex matching on subsequent modules.
|
|
39
|
+
*
|
|
40
|
+
* @param code - The source code of the module being transformed
|
|
41
|
+
* @returns `null` to indicate no transformation is needed (we're only analyzing the code)
|
|
42
|
+
*/
|
|
43
|
+
transform(code) {
|
|
44
|
+
if (foundHubspotImport) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
if (!code.includes('@hubspot/ui-extensions')) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (HUBSPOT_IMPORT_REGEX.test(code) ||
|
|
51
|
+
NAMESPACE_IMPORT_REGEX.test(code)) {
|
|
52
|
+
foundHubspotImport = true;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
},
|
|
56
|
+
/**
|
|
57
|
+
* Called by Rollup once at the end of the build, after all modules have been processed
|
|
58
|
+
* and the build is complete. This hook is invoked regardless of whether the build
|
|
59
|
+
* succeeded or failed, making it useful for cleanup tasks or final validation checks.
|
|
60
|
+
*
|
|
61
|
+
* In watch mode, this hook is called after each rebuild completes.
|
|
62
|
+
*
|
|
63
|
+
* We use this hook to check if we found the required `hubspot` import during the build.
|
|
64
|
+
* If not found, we log a warning to help developers understand why their extension
|
|
65
|
+
* might not render correctly.
|
|
66
|
+
*/
|
|
67
|
+
buildEnd() {
|
|
68
|
+
if (!foundHubspotImport) {
|
|
69
|
+
logger.warn(`
|
|
70
|
+
|
|
71
|
+
${chalk.yellow.bold('WARNING:')} Your extension does not appear to import ${chalk.cyan('hubspot')} from ${chalk.cyan('@hubspot/ui-extensions')}.
|
|
72
|
+
|
|
73
|
+
${chalk.red('Without this import, your extension will not render.')} To fix this:
|
|
74
|
+
|
|
75
|
+
${chalk.bold('1.')} Import ${chalk.cyan('hubspot')} from ${chalk.cyan('@hubspot/ui-extensions')}
|
|
76
|
+
${chalk.bold('2.')} Call ${chalk.cyan('hubspot.extend()')} with your render function
|
|
77
|
+
|
|
78
|
+
${chalk.bold('Example:')}
|
|
79
|
+
${chalk.gray('import')} ${chalk.cyan('{ hubspot }')} ${chalk.gray('from')} ${chalk.yellow("'@hubspot/ui-extensions'")};
|
|
80
|
+
|
|
81
|
+
${chalk.cyan('hubspot')}.${chalk.cyan('extend')}${chalk.gray("<'crm.record.tab'>")}((${chalk.gray('{ context }')}) => {
|
|
82
|
+
${chalk.gray('return')} <MyExtension />;
|
|
83
|
+
});
|
|
84
|
+
`);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
export default codeCheckingPlugin;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as Vite from 'vite';
|
|
2
|
+
import { DevServerState } from '../DevServerState.ts';
|
|
3
|
+
export interface DevBuildPluginOptions {
|
|
4
|
+
devServerState: DevServerState;
|
|
5
|
+
}
|
|
6
|
+
export type DevBuildPlugin = (options: DevBuildPluginOptions) => Vite.Plugin;
|
|
7
|
+
declare const devBuildPlugin: DevBuildPlugin;
|
|
8
|
+
export default devBuildPlugin;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { ROLLUP_OPTIONS, WEBSOCKET_MESSAGE_VERSION } from "../constants.js";
|
|
2
|
+
import { build } from 'vite';
|
|
3
|
+
import manifestPlugin from "./manifestPlugin.js";
|
|
4
|
+
import { stripAnsiColorCodes } from "../utils.js";
|
|
5
|
+
import codeCheckingPlugin from "./codeCheckingPlugin.js";
|
|
6
|
+
import friendlyLoggingPlugin from "./friendlyLoggingPlugin.js";
|
|
7
|
+
import relevantModulesPlugin, { getRelevantModules, } from "./relevantModulesPlugin.js";
|
|
8
|
+
import codeBlockingPlugin from "./codeBlockingPlugin.js";
|
|
9
|
+
function addVersionToBaseMessage(baseMessage) {
|
|
10
|
+
return {
|
|
11
|
+
...baseMessage,
|
|
12
|
+
version: WEBSOCKET_MESSAGE_VERSION,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function isValidVariablesRecord(obj) {
|
|
16
|
+
if (!obj || typeof obj !== 'object')
|
|
17
|
+
return false;
|
|
18
|
+
return Object.entries(obj).every(([key, value]) => typeof key === 'string' &&
|
|
19
|
+
(typeof value === 'string' ||
|
|
20
|
+
typeof value === 'number' ||
|
|
21
|
+
typeof value === 'boolean'));
|
|
22
|
+
}
|
|
23
|
+
const devBuildPlugin = (options) => {
|
|
24
|
+
const { devServerState } = options;
|
|
25
|
+
const { logger } = devServerState;
|
|
26
|
+
let lastBuildErrorContext;
|
|
27
|
+
const handleBuildError = (error) => {
|
|
28
|
+
const { error: { plugin, errors, frame, loc, id }, extensionMetadata, } = error;
|
|
29
|
+
// Filter out our custom plugins, but send everything else
|
|
30
|
+
if (!plugin?.startsWith('ui-extensions')) {
|
|
31
|
+
const ws = devServerState.extensionsWebSocket;
|
|
32
|
+
/**
|
|
33
|
+
* If there's no WebSocket, there are no connected clients to notify of the error.
|
|
34
|
+
* In this case, the error data is already stored in `lastBuildErrorContext` to be sent when a client connects.
|
|
35
|
+
*
|
|
36
|
+
* If there is a WebSocket, broadcast the error to all connected clients
|
|
37
|
+
*/
|
|
38
|
+
if (ws) {
|
|
39
|
+
devServerState.getExtensionsWebSocket().broadcast({
|
|
40
|
+
...addVersionToBaseMessage(extensionMetadata.baseMessage),
|
|
41
|
+
event: 'error',
|
|
42
|
+
error: {
|
|
43
|
+
details: {
|
|
44
|
+
errors,
|
|
45
|
+
formattedError: stripAnsiColorCodes(frame),
|
|
46
|
+
location: loc,
|
|
47
|
+
file: id,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const devBuild = async (server, extensionMetadata, emptyOutDir = false) => {
|
|
55
|
+
try {
|
|
56
|
+
const { config: extensionConfig } = extensionMetadata;
|
|
57
|
+
const { extensionPath } = extensionConfig;
|
|
58
|
+
let manifestConfig = {};
|
|
59
|
+
if (devServerState.appConfig &&
|
|
60
|
+
'variables' in devServerState.appConfig &&
|
|
61
|
+
isValidVariablesRecord(devServerState.appConfig.variables)) {
|
|
62
|
+
manifestConfig = {
|
|
63
|
+
variables: devServerState.appConfig.variables,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
await build({
|
|
67
|
+
logLevel: 'warn',
|
|
68
|
+
mode: 'development',
|
|
69
|
+
define: {
|
|
70
|
+
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
|
|
71
|
+
},
|
|
72
|
+
esbuild: {
|
|
73
|
+
tsconfigRaw: {
|
|
74
|
+
compilerOptions: {
|
|
75
|
+
preserveValueImports: true,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
build: {
|
|
80
|
+
lib: {
|
|
81
|
+
entry: extensionConfig.data.module.file,
|
|
82
|
+
name: extensionConfig.output,
|
|
83
|
+
formats: ['iife'],
|
|
84
|
+
fileName: () => extensionConfig.output,
|
|
85
|
+
},
|
|
86
|
+
rollupOptions: {
|
|
87
|
+
...ROLLUP_OPTIONS,
|
|
88
|
+
plugins: [
|
|
89
|
+
manifestPlugin({
|
|
90
|
+
minify: false,
|
|
91
|
+
output: extensionConfig.output,
|
|
92
|
+
extensionPath,
|
|
93
|
+
logger,
|
|
94
|
+
manifestConfig,
|
|
95
|
+
}),
|
|
96
|
+
codeCheckingPlugin({
|
|
97
|
+
logger,
|
|
98
|
+
}),
|
|
99
|
+
friendlyLoggingPlugin({ logger }),
|
|
100
|
+
relevantModulesPlugin({
|
|
101
|
+
output: extensionConfig.output,
|
|
102
|
+
logger,
|
|
103
|
+
}),
|
|
104
|
+
codeBlockingPlugin({ logger, extensionPath }),
|
|
105
|
+
],
|
|
106
|
+
output: {
|
|
107
|
+
...ROLLUP_OPTIONS.output,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
outDir: devServerState.outputDir,
|
|
111
|
+
emptyOutDir,
|
|
112
|
+
minify: false,
|
|
113
|
+
sourcemap: 'inline',
|
|
114
|
+
},
|
|
115
|
+
clearScreen: false,
|
|
116
|
+
});
|
|
117
|
+
lastBuildErrorContext = null;
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
lastBuildErrorContext = {
|
|
122
|
+
error: error,
|
|
123
|
+
extensionMetadata,
|
|
124
|
+
};
|
|
125
|
+
logger.debug(error);
|
|
126
|
+
handleBuildError(lastBuildErrorContext);
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
let localServer;
|
|
131
|
+
return {
|
|
132
|
+
name: 'ui-extensions-dev-build-plugin',
|
|
133
|
+
enforce: 'pre',
|
|
134
|
+
configureServer: async (server) => {
|
|
135
|
+
// Store a reference to the server to be used in hooks that don't get the server injected
|
|
136
|
+
// See https://vitejs.dev/guide/api-plugin.html#configureserver for information on this pattern
|
|
137
|
+
localServer = server;
|
|
138
|
+
// Store the WebSocket setup to be called after the WebSocket is initialized
|
|
139
|
+
devServerState.setWebSocketSetupCallback(() => {
|
|
140
|
+
devServerState.getExtensionsWebSocket().onConnection(() => {
|
|
141
|
+
logger.info('Browser connected and listening for bundle updates');
|
|
142
|
+
devServerState.extensionsMetadata.forEach((metadata) => {
|
|
143
|
+
devServerState.getExtensionsWebSocket().broadcast({
|
|
144
|
+
...addVersionToBaseMessage(metadata.baseMessage),
|
|
145
|
+
event: 'start',
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
if (lastBuildErrorContext) {
|
|
149
|
+
handleBuildError(lastBuildErrorContext);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
for (let i = 0; i < devServerState.extensionsMetadata.length; ++i) {
|
|
154
|
+
await devBuild(localServer, devServerState.extensionsMetadata[i], i === 0);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
handleHotUpdate: async ({ file, server }) => {
|
|
158
|
+
// If the file is not in the relevantModules list, it's update is inconsequential
|
|
159
|
+
const extensionsToRebuild = devServerState.extensionsMetadata.filter((metadata) => {
|
|
160
|
+
const { config } = metadata;
|
|
161
|
+
return getRelevantModules(config.output).includes(file);
|
|
162
|
+
});
|
|
163
|
+
for (let i = 0; i < extensionsToRebuild.length; ++i) {
|
|
164
|
+
const toRebuild = extensionsToRebuild[i];
|
|
165
|
+
const successful = await devBuild(server, toRebuild);
|
|
166
|
+
if (!successful) {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
const { config: extensionConfig } = toRebuild;
|
|
170
|
+
logger.info(`Extension ${extensionConfig.data.title} updated, compiled`);
|
|
171
|
+
const ws = devServerState.getExtensionsWebSocket();
|
|
172
|
+
if (ws.clientCount === 0) {
|
|
173
|
+
logger.debug('Bundle updated, no browsers connected to notify');
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
logger.debug('Bundle updated, notifying connected browsers');
|
|
177
|
+
ws.broadcast({
|
|
178
|
+
...addVersionToBaseMessage(toRebuild.baseMessage),
|
|
179
|
+
event: 'update',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return [];
|
|
183
|
+
},
|
|
184
|
+
buildEnd(error) {
|
|
185
|
+
if (error) {
|
|
186
|
+
logger.error(error);
|
|
187
|
+
}
|
|
188
|
+
const ws = devServerState.extensionsWebSocket;
|
|
189
|
+
if (ws) {
|
|
190
|
+
logger.debug('Sending shutdown message to connected browsers');
|
|
191
|
+
devServerState.extensionsMetadata.forEach((metadata) => {
|
|
192
|
+
ws.broadcast({
|
|
193
|
+
...addVersionToBaseMessage(metadata.baseMessage),
|
|
194
|
+
event: 'shutdown',
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
export default devBuildPlugin;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { RollupLog, LogLevel } from 'rollup';
|
|
2
|
+
import * as Vite from 'vite';
|
|
3
|
+
import { Logger } from '../types.ts';
|
|
4
|
+
export interface MappedLog {
|
|
5
|
+
message?: string;
|
|
6
|
+
level?: 'error' | LogLevel;
|
|
7
|
+
}
|
|
8
|
+
export interface CodeToLogMapper {
|
|
9
|
+
[key: string]: (loggable: RollupLog) => MappedLog;
|
|
10
|
+
}
|
|
11
|
+
declare function friendlyLoggingPlugin({ logger }: {
|
|
12
|
+
logger: Logger;
|
|
13
|
+
}): Vite.Plugin;
|
|
14
|
+
export default friendlyLoggingPlugin;
|