@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.
Files changed (95) hide show
  1. package/dist/index.d.ts +4 -0
  2. package/dist/index.js +4 -0
  3. package/dist/lib/DevModeInterface.d.ts +9 -0
  4. package/dist/lib/DevModeInterface.js +36 -0
  5. package/dist/lib/DevModeParentInterface.d.ts +19 -0
  6. package/dist/lib/DevModeParentInterface.js +181 -0
  7. package/dist/lib/DevModeUnifiedInterface.d.ts +9 -0
  8. package/dist/lib/DevModeUnifiedInterface.js +118 -0
  9. package/dist/lib/DevServerState.d.ts +44 -0
  10. package/dist/lib/DevServerState.js +95 -0
  11. package/dist/lib/ExtensionsWebSocket.d.ts +25 -0
  12. package/dist/lib/ExtensionsWebSocket.js +110 -0
  13. package/dist/lib/__mocks__/config.d.ts +2 -0
  14. package/dist/lib/__mocks__/config.js +5 -0
  15. package/dist/lib/__mocks__/isExtensionFile.d.ts +5 -0
  16. package/dist/lib/__mocks__/isExtensionFile.js +11 -0
  17. package/dist/lib/__tests__/DevModeInterface.spec.d.ts +1 -0
  18. package/dist/lib/__tests__/DevModeInterface.spec.js +155 -0
  19. package/dist/lib/__tests__/DevModeParentInterface.spec.d.ts +1 -0
  20. package/dist/lib/__tests__/DevModeParentInterface.spec.js +179 -0
  21. package/dist/lib/__tests__/DevModeUnifiedInterface.spec.d.ts +1 -0
  22. package/dist/lib/__tests__/DevModeUnifiedInterface.spec.js +236 -0
  23. package/dist/lib/__tests__/ExtensionsWebSocket.spec.d.ts +1 -0
  24. package/dist/lib/__tests__/ExtensionsWebSocket.spec.js +304 -0
  25. package/dist/lib/__tests__/ast.spec.d.ts +1 -0
  26. package/dist/lib/__tests__/ast.spec.js +737 -0
  27. package/dist/lib/__tests__/build.spec.d.ts +1 -0
  28. package/dist/lib/__tests__/build.spec.js +159 -0
  29. package/dist/lib/__tests__/config.spec.d.ts +1 -0
  30. package/dist/lib/__tests__/config.spec.js +291 -0
  31. package/dist/lib/__tests__/dev.spec.d.ts +1 -0
  32. package/dist/lib/__tests__/dev.spec.js +80 -0
  33. package/dist/lib/__tests__/extensionsService.spec.d.ts +1 -0
  34. package/dist/lib/__tests__/extensionsService.spec.js +150 -0
  35. package/dist/lib/__tests__/factories.d.ts +48 -0
  36. package/dist/lib/__tests__/factories.js +32 -0
  37. package/dist/lib/__tests__/fixtures/extensionConfig.d.ts +182 -0
  38. package/dist/lib/__tests__/fixtures/extensionConfig.js +304 -0
  39. package/dist/lib/__tests__/fixtures/urls.d.ts +4 -0
  40. package/dist/lib/__tests__/fixtures/urls.js +4 -0
  41. package/dist/lib/__tests__/parsing-utils.spec.d.ts +1 -0
  42. package/dist/lib/__tests__/parsing-utils.spec.js +467 -0
  43. package/dist/lib/__tests__/plugins/codeBlockingPlugin.spec.d.ts +1 -0
  44. package/dist/lib/__tests__/plugins/codeBlockingPlugin.spec.js +112 -0
  45. package/dist/lib/__tests__/plugins/codeCheckingPlugin.spec.d.ts +1 -0
  46. package/dist/lib/__tests__/plugins/codeCheckingPlugin.spec.js +124 -0
  47. package/dist/lib/__tests__/plugins/devBuildPlugin.spec.d.ts +1 -0
  48. package/dist/lib/__tests__/plugins/devBuildPlugin.spec.js +396 -0
  49. package/dist/lib/__tests__/plugins/friendlyLoggingPlugin.spec.d.ts +1 -0
  50. package/dist/lib/__tests__/plugins/friendlyLoggingPlugin.spec.js +65 -0
  51. package/dist/lib/__tests__/plugins/manifestPlugin.spec.d.ts +1 -0
  52. package/dist/lib/__tests__/plugins/manifestPlugin.spec.js +455 -0
  53. package/dist/lib/__tests__/plugins/relevantModulesPlugin.spec.d.ts +1 -0
  54. package/dist/lib/__tests__/plugins/relevantModulesPlugin.spec.js +115 -0
  55. package/dist/lib/__tests__/server.spec.d.ts +1 -0
  56. package/dist/lib/__tests__/server.spec.js +152 -0
  57. package/dist/lib/__tests__/test-utils/ast.d.ts +1 -0
  58. package/dist/lib/__tests__/test-utils/ast.js +4 -0
  59. package/dist/lib/__tests__/utils.spec.d.ts +1 -0
  60. package/dist/lib/__tests__/utils.spec.js +176 -0
  61. package/dist/lib/ast.d.ts +16 -0
  62. package/dist/lib/ast.js +281 -0
  63. package/dist/lib/bin/cli.d.ts +2 -0
  64. package/dist/lib/bin/cli.js +143 -0
  65. package/dist/lib/build.d.ts +24 -0
  66. package/dist/lib/build.js +73 -0
  67. package/dist/lib/config.d.ts +7 -0
  68. package/dist/lib/config.js +124 -0
  69. package/dist/lib/constants.d.ts +32 -0
  70. package/dist/lib/constants.js +43 -0
  71. package/dist/lib/dev.d.ts +2 -0
  72. package/dist/lib/dev.js +58 -0
  73. package/dist/lib/extensionsService.d.ts +10 -0
  74. package/dist/lib/extensionsService.js +45 -0
  75. package/dist/lib/parsing-utils.d.ts +31 -0
  76. package/dist/lib/parsing-utils.js +289 -0
  77. package/dist/lib/plugins/codeBlockingPlugin.d.ts +8 -0
  78. package/dist/lib/plugins/codeBlockingPlugin.js +45 -0
  79. package/dist/lib/plugins/codeCheckingPlugin.d.ts +8 -0
  80. package/dist/lib/plugins/codeCheckingPlugin.js +93 -0
  81. package/dist/lib/plugins/devBuildPlugin.d.ts +8 -0
  82. package/dist/lib/plugins/devBuildPlugin.js +212 -0
  83. package/dist/lib/plugins/friendlyLoggingPlugin.d.ts +14 -0
  84. package/dist/lib/plugins/friendlyLoggingPlugin.js +36 -0
  85. package/dist/lib/plugins/manifestPlugin.d.ts +12 -0
  86. package/dist/lib/plugins/manifestPlugin.js +158 -0
  87. package/dist/lib/plugins/relevantModulesPlugin.d.ts +14 -0
  88. package/dist/lib/plugins/relevantModulesPlugin.js +33 -0
  89. package/dist/lib/server.d.ts +13 -0
  90. package/dist/lib/server.js +99 -0
  91. package/dist/lib/types.d.ts +290 -0
  92. package/dist/lib/types.js +12 -0
  93. package/dist/lib/utils.d.ts +25 -0
  94. package/dist/lib/utils.js +113 -0
  95. package/package.json +2 -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,93 @@
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(error) {
68
+ // Don't display the warning if there was a build error
69
+ if (error) {
70
+ return;
71
+ }
72
+ if (!foundHubspotImport) {
73
+ logger.warn(`
74
+
75
+ ${chalk.yellow.bold('WARNING:')} Your extension does not appear to import ${chalk.cyan('hubspot')} from ${chalk.cyan('@hubspot/ui-extensions')}.
76
+
77
+ ${chalk.red('Without this import, your extension will not render.')} To fix this:
78
+
79
+ ${chalk.bold('1.')} Import ${chalk.cyan('hubspot')} from ${chalk.cyan('@hubspot/ui-extensions')}
80
+ ${chalk.bold('2.')} Call ${chalk.cyan('hubspot.extend()')} with your render function
81
+
82
+ ${chalk.bold('Example:')}
83
+ ${chalk.gray('import')} ${chalk.cyan('{ hubspot }')} ${chalk.gray('from')} ${chalk.yellow("'@hubspot/ui-extensions'")};
84
+
85
+ ${chalk.cyan('hubspot')}.${chalk.cyan('extend')}${chalk.gray("<'crm.record.tab'>")}((${chalk.gray('{ context }')}) => {
86
+ ${chalk.gray('return')} <MyExtension />;
87
+ });
88
+ `);
89
+ }
90
+ },
91
+ };
92
+ };
93
+ 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,212 @@
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, addRelevantModule, } 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
+ const { config: extensionConfig } = extensionMetadata;
56
+ const { extensionPath } = extensionConfig;
57
+ try {
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
+ // Log the error for user visibility
126
+ const errorPayload = error;
127
+ if (errorPayload.message) {
128
+ logger.error(errorPayload.message);
129
+ }
130
+ // Ensure the files are tracked for HMR even when the build fails
131
+ const entryFile = extensionConfig.data.module.file;
132
+ addRelevantModule(extensionConfig.output, entryFile);
133
+ if (errorPayload.id && errorPayload.id !== entryFile) {
134
+ addRelevantModule(extensionConfig.output, errorPayload.id);
135
+ }
136
+ logger.debug('Full build error:', error);
137
+ handleBuildError(lastBuildErrorContext);
138
+ return false;
139
+ }
140
+ };
141
+ let localServer;
142
+ return {
143
+ name: 'ui-extensions-dev-build-plugin',
144
+ enforce: 'pre',
145
+ configureServer: async (server) => {
146
+ // Store a reference to the server to be used in hooks that don't get the server injected
147
+ // See https://vitejs.dev/guide/api-plugin.html#configureserver for information on this pattern
148
+ localServer = server;
149
+ // Store the WebSocket setup to be called after the WebSocket is initialized
150
+ devServerState.setWebSocketSetupCallback(() => {
151
+ devServerState.getExtensionsWebSocket().onConnection(() => {
152
+ logger.info('Browser connected and listening for bundle updates');
153
+ devServerState.extensionsMetadata.forEach((metadata) => {
154
+ devServerState.getExtensionsWebSocket().broadcast({
155
+ ...addVersionToBaseMessage(metadata.baseMessage),
156
+ event: 'start',
157
+ });
158
+ });
159
+ if (lastBuildErrorContext) {
160
+ handleBuildError(lastBuildErrorContext);
161
+ }
162
+ });
163
+ });
164
+ for (let i = 0; i < devServerState.extensionsMetadata.length; ++i) {
165
+ await devBuild(localServer, devServerState.extensionsMetadata[i], i === 0);
166
+ }
167
+ },
168
+ handleHotUpdate: async ({ file, server }) => {
169
+ // If the file is not in the relevantModules list, it's update is inconsequential
170
+ const extensionsToRebuild = devServerState.extensionsMetadata.filter((metadata) => {
171
+ const { config } = metadata;
172
+ return getRelevantModules(config.output).includes(file);
173
+ });
174
+ for (let i = 0; i < extensionsToRebuild.length; ++i) {
175
+ const toRebuild = extensionsToRebuild[i];
176
+ const successful = await devBuild(server, toRebuild);
177
+ if (!successful) {
178
+ return [];
179
+ }
180
+ const { config: extensionConfig } = toRebuild;
181
+ logger.info(`Extension ${extensionConfig.data.title} updated, compiled`);
182
+ const ws = devServerState.getExtensionsWebSocket();
183
+ if (ws.clientCount === 0) {
184
+ logger.debug('Bundle updated, no browsers connected to notify');
185
+ return [];
186
+ }
187
+ logger.debug('Bundle updated, notifying connected browsers');
188
+ ws.broadcast({
189
+ ...addVersionToBaseMessage(toRebuild.baseMessage),
190
+ event: 'update',
191
+ });
192
+ }
193
+ return [];
194
+ },
195
+ buildEnd(error) {
196
+ if (error) {
197
+ logger.error(error);
198
+ }
199
+ const ws = devServerState.extensionsWebSocket;
200
+ if (ws) {
201
+ logger.debug('Sending shutdown message to connected browsers');
202
+ devServerState.extensionsMetadata.forEach((metadata) => {
203
+ ws.broadcast({
204
+ ...addVersionToBaseMessage(metadata.baseMessage),
205
+ event: 'shutdown',
206
+ });
207
+ });
208
+ }
209
+ },
210
+ };
211
+ };
212
+ export default devBuildPlugin;