@hubspot/ui-extensions-dev-server 0.9.2 → 0.9.4

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.
@@ -12,7 +12,7 @@ export declare abstract class DevModeParentInterface {
12
12
  protected abstract _generateAppExtensionMappings(components: ProjectComponentMap | UnifiedProjectComponentMap): AppExtensionMapping[];
13
13
  _getPlatformVersion(projectConfig?: ProjectConfig): PlatformVersion;
14
14
  _reset(): void;
15
- parentSetup({ onUploadRequired, promptUser, logger, urls, setActiveApp, choices, }: DevModeBaseSetupArguments): Promise<void>;
15
+ parentSetup({ onUploadRequired, logger, urls, setActiveApp, choices, }: DevModeBaseSetupArguments): Promise<void>;
16
16
  start({ requestPorts, accountId, projectConfig, }: DevModeStartArguments): Promise<void>;
17
17
  fileChange(filePath: string, __event: unknown): Promise<void>;
18
18
  cleanup(): Promise<void>;
@@ -8,6 +8,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
11
14
  Object.defineProperty(exports, "__esModule", { value: true });
12
15
  exports.DevModeParentInterface = void 0;
13
16
  const constants_1 = require("./constants");
@@ -16,6 +19,7 @@ const config_1 = require("./config");
16
19
  const constants_2 = require("./constants");
17
20
  const utils_1 = require("./utils");
18
21
  const DevServerState_1 = require("./DevServerState");
22
+ const inquirer_1 = __importDefault(require("inquirer"));
19
23
  const defaultLogger = {
20
24
  info: (...args) => {
21
25
  console.log(...args);
@@ -63,7 +67,7 @@ class DevModeParentInterface {
63
67
  this.isConfigured = false;
64
68
  this.isRunning = false;
65
69
  }
66
- parentSetup({ onUploadRequired, promptUser, logger, urls, setActiveApp, choices = [], }) {
70
+ parentSetup({ onUploadRequired, logger, urls, setActiveApp, choices = [], }) {
67
71
  var _a, _b, _c;
68
72
  return __awaiter(this, void 0, void 0, function* () {
69
73
  if (this.isConfigured) {
@@ -80,7 +84,8 @@ class DevModeParentInterface {
80
84
  this.configs = [choices[0].value];
81
85
  }
82
86
  else {
83
- const answers = yield promptUser({
87
+ const promptModule = inquirer_1.default.createPromptModule();
88
+ const answers = yield promptModule({
84
89
  type: 'checkbox',
85
90
  name: 'extensions',
86
91
  message: 'Which extension(s) would you like to run?',
package/dist/lib/ast.d.ts CHANGED
@@ -1,5 +1,11 @@
1
- import { SourceCodeMetadata, SourceCodeChecks, Logger } from './types';
1
+ import { SourceCodeMetadata, SourceCodeChecks, Logger, NodeValue } from './types';
2
2
  import { Program, Node } from 'estree';
3
+ /**
4
+ * This is a simple utility function to rebuild the value from a node.
5
+ * It'll work for simple stuff, but it's likely to fail in complicated cases.
6
+ * Use with caution!
7
+ */
8
+ export declare function getValueFromNode(node: Node, state: SourceCodeMetadata): NodeValue;
3
9
  /**
4
10
  * We only support image imports that are within the extension directory.
5
11
  * This function will check if an image is out of bounds and collect any that are out of bounds, so we can warn the user before they run into build issues.
@@ -12,4 +18,5 @@ export declare function traverseAbstractSyntaxTree(ast: Program, checks: SourceC
12
18
  importedHooks: {};
13
19
  dependencies: never[];
14
20
  };
21
+ variableDeclarations: Map<string, NodeValue>;
15
22
  };
package/dist/lib/ast.js CHANGED
@@ -4,7 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.traverseAbstractSyntaxTree = exports.checkForOutOfBoundsImageImports = void 0;
7
+ exports.traverseAbstractSyntaxTree = exports.checkForOutOfBoundsImageImports = exports.getValueFromNode = void 0;
8
+ const UNSUPPORTED_SPREAD = Symbol('unsupported-spread');
8
9
  const path_1 = __importDefault(require("path"));
9
10
  // @ts-expect-error no type defs
10
11
  const estraverse_1 = require("estraverse");
@@ -53,6 +54,205 @@ function _checkForFunctionMetadata(node, parent, output, functionName) {
53
54
  output.functions[functionName].defined = true;
54
55
  }
55
56
  }
57
+ function _collectVariableDeclarations(node, state) {
58
+ if (!node) {
59
+ return;
60
+ }
61
+ // Handle variable declarations (const, let, var)
62
+ if (node.type === 'VariableDeclaration') {
63
+ node.declarations.forEach((declaration) => {
64
+ if (declaration.type === 'VariableDeclarator' &&
65
+ declaration.id.type === 'Identifier' &&
66
+ declaration.init) {
67
+ const variableName = declaration.id.name;
68
+ const value = getValueFromNode(declaration.init, state);
69
+ state.variableDeclarations.set(variableName, value);
70
+ }
71
+ });
72
+ }
73
+ // Handle assignment expressions for let variables (e.g., myVar = newValue)
74
+ if (node.type === 'AssignmentExpression' && node.left.type === 'Identifier') {
75
+ const variableName = node.left.name;
76
+ if (state.variableDeclarations.has(variableName)) {
77
+ const value = getValueFromNode(node.right, state);
78
+ state.variableDeclarations.set(variableName, value);
79
+ }
80
+ }
81
+ }
82
+ /**
83
+ * This is a simple utility function to rebuild the value from a node.
84
+ * It'll work for simple stuff, but it's likely to fail in complicated cases.
85
+ * Use with caution!
86
+ */
87
+ function getValueFromNode(node, state) {
88
+ switch (node.type) {
89
+ case 'Literal':
90
+ return node.value;
91
+ case 'Identifier': {
92
+ const name = node.name;
93
+ switch (name) {
94
+ case 'undefined':
95
+ return undefined;
96
+ case 'NaN':
97
+ return NaN;
98
+ case 'Infinity':
99
+ return Infinity;
100
+ default:
101
+ if (state.variableDeclarations.has(name)) {
102
+ return state.variableDeclarations.get(name);
103
+ }
104
+ return name;
105
+ }
106
+ }
107
+ case 'ArrayExpression': {
108
+ const arrayValue = [];
109
+ if (node.elements.length === 0) {
110
+ return arrayValue;
111
+ }
112
+ // Arrays have to be built from their elements, to handle special cases like nested arrays from spread operators.
113
+ node.elements.forEach((element) => {
114
+ if (typeof element === 'object' && element !== null) {
115
+ if (element.type === 'SpreadElement') {
116
+ const value = getValueFromNode(element, state);
117
+ if (Array.isArray(value)) {
118
+ arrayValue.push(...value);
119
+ }
120
+ else {
121
+ arrayValue.push(value);
122
+ }
123
+ }
124
+ else {
125
+ const value = getValueFromNode(element, state);
126
+ arrayValue.push(value);
127
+ }
128
+ }
129
+ else {
130
+ arrayValue.push(element);
131
+ }
132
+ });
133
+ return arrayValue;
134
+ }
135
+ case 'ObjectExpression': {
136
+ const obj = {};
137
+ node.properties.forEach((prop) => {
138
+ switch (prop.type) {
139
+ case 'Property': {
140
+ const property = prop;
141
+ let key = undefined;
142
+ if (property.key.type === 'Identifier') {
143
+ key = property.key.name;
144
+ }
145
+ else if (property.key.type === 'Literal') {
146
+ key = String(property.key.value);
147
+ }
148
+ if (key) {
149
+ obj[key] = getValueFromNode(property.value, state);
150
+ }
151
+ break;
152
+ }
153
+ case 'SpreadElement': {
154
+ const spreadValue = getValueFromNode(prop, state);
155
+ if (spreadValue !== UNSUPPORTED_SPREAD &&
156
+ spreadValue &&
157
+ typeof spreadValue === 'object') {
158
+ Object.assign(obj, spreadValue);
159
+ }
160
+ break;
161
+ }
162
+ default:
163
+ // Ignore unsupported property types, as we don't have a key for them.
164
+ // This could be a computed property or something else we don't handle.
165
+ break;
166
+ }
167
+ });
168
+ return obj;
169
+ }
170
+ /**
171
+ * Spread elements are a bit tricky. They can be used to directly spread an array or object,
172
+ * or they can be used to spread a variable that is defined elsewhere. Our strategy is to return
173
+ * whatever element should be spread, and then handle the spreading in the parent array or object.
174
+ *
175
+ * There are also trickier cases we don't handle, like spreading a function call that returns an
176
+ * array or object. When the spread element is unsupported, we return a special symbol.
177
+ */
178
+ case 'SpreadElement':
179
+ if (node.argument) {
180
+ if (node.argument.type === 'Identifier' && node.argument.name) {
181
+ return state.variableDeclarations.get(node.argument.name) || null;
182
+ }
183
+ else if (node.argument.type === 'ArrayExpression' ||
184
+ node.argument.type === 'ObjectExpression') {
185
+ return getValueFromNode(node.argument, state);
186
+ }
187
+ }
188
+ return UNSUPPORTED_SPREAD;
189
+ /**
190
+ * Template literals are built of an alternating sequence of static
191
+ * strings and expressions. We will concatenate the static strings and
192
+ * evaluate the expressions to build the final string.
193
+ */
194
+ case 'TemplateLiteral': {
195
+ let result = '';
196
+ if (node.expressions && node.quasis) {
197
+ const { quasis, expressions } = node;
198
+ for (let i = 0; i < quasis.length; i++) {
199
+ // Prefer cooked value if available, otherwise use raw.
200
+ result += quasis[i].value.cooked || quasis[i].value.raw;
201
+ if (i < expressions.length) {
202
+ const expression = expressions[i];
203
+ const expressionValue = getValueFromNode(expression, state);
204
+ if (expressionValue === null ||
205
+ expressionValue === undefined ||
206
+ typeof expressionValue === 'string' ||
207
+ typeof expressionValue === 'number' ||
208
+ typeof expressionValue === 'boolean') {
209
+ result += String(expressionValue);
210
+ }
211
+ else if (Array.isArray(expressionValue)) {
212
+ result += expressionValue.join(',');
213
+ }
214
+ else if (typeof expressionValue === 'object') {
215
+ result += '[object Object]';
216
+ }
217
+ }
218
+ }
219
+ }
220
+ return result;
221
+ }
222
+ case 'UnaryExpression': {
223
+ const arg = getValueFromNode(node.argument, state);
224
+ switch (node.operator) {
225
+ case '-':
226
+ return typeof arg === 'number' ? -arg : NaN;
227
+ case '+':
228
+ return typeof arg === 'number' ? +arg : NaN;
229
+ case '!':
230
+ return !arg;
231
+ case 'typeof':
232
+ return typeof arg;
233
+ default:
234
+ return undefined;
235
+ }
236
+ }
237
+ // Member expressions are used to access properties of objects.
238
+ case 'MemberExpression': {
239
+ if (node.property.type === 'Identifier' && node.property.name) {
240
+ // We have to recursively get the value of the object, due to the way that nested objects are parsed.
241
+ const objectData = getValueFromNode(node.object, state);
242
+ if (objectData &&
243
+ typeof objectData === 'object' &&
244
+ !Array.isArray(objectData) &&
245
+ !(objectData instanceof RegExp)) {
246
+ return objectData[node.property.name];
247
+ }
248
+ }
249
+ return undefined;
250
+ }
251
+ default:
252
+ return `Unsupported node type: ${node.type}`;
253
+ }
254
+ }
255
+ exports.getValueFromNode = getValueFromNode;
56
256
  /**
57
257
  * We only support image imports that are within the extension directory.
58
258
  * This function will check if an image is out of bounds and collect any that are out of bounds, so we can warn the user before they run into build issues.
@@ -132,40 +332,47 @@ function _collectDataDependencies(node, output, logger) {
132
332
  // Then we handle each hook individually, as the usages and tracking format are different.
133
333
  if (hookName === 'useCrmProperties') {
134
334
  const propertyType = 'CrmRecordProperties';
135
- // Get the first argument, the properties array
136
335
  const propertiesNode = node.arguments[0];
336
+ const optionsNode = node.arguments[1];
137
337
  const requestedProperties = [];
138
- // If the first argument is an array with at least one element, collect the properties.
139
- if (propertiesNode &&
140
- propertiesNode.type === 'ArrayExpression' &&
141
- propertiesNode.elements.length > 0) {
142
- propertiesNode.elements.forEach((element) => {
143
- /**
144
- * We only support strings for now, and ignore the rest.
145
- * This might be more generalized in the future as we support more hooks.
146
- */
147
- if (element &&
148
- element.type === 'Literal' &&
149
- typeof element.value === 'string') {
150
- requestedProperties.push(element.value);
151
- }
152
- else {
153
- logger.warn(`Invalid property type in useCrmProperties: ${element ? element.type : 'undefined'}`);
338
+ const resolveValue = (astNode) => {
339
+ if (!astNode)
340
+ return null;
341
+ if (astNode.type === 'Identifier' &&
342
+ output.variableDeclarations.has(astNode.name)) {
343
+ return output.variableDeclarations.get(astNode.name);
344
+ }
345
+ return getValueFromNode(astNode, output);
346
+ };
347
+ const propertiesValue = resolveValue(propertiesNode);
348
+ if (propertiesValue && Array.isArray(propertiesValue)) {
349
+ propertiesValue.forEach((val) => {
350
+ if (typeof val === 'string') {
351
+ requestedProperties.push(val);
154
352
  }
155
353
  });
156
- if (requestedProperties.length > 0) {
157
- output.dataDependencies.dependencies.push({
158
- /**
159
- * This refID is a hash of the property type and the requested properties.
160
- * This should allow us to create the same hash at execution time, to find the correct data from BE.
161
- */
162
- referenceId: (0, utils_1.generateHash)(propertyType, requestedProperties),
163
- properties: {
164
- type: propertyType,
165
- recordProperties: requestedProperties,
166
- },
167
- });
354
+ }
355
+ if (requestedProperties.length > 0) {
356
+ let options = {};
357
+ const optionsValue = resolveValue(optionsNode);
358
+ if (optionsValue &&
359
+ typeof optionsValue === 'object' &&
360
+ !Array.isArray(optionsValue) &&
361
+ !(optionsValue instanceof RegExp)) {
362
+ options = optionsValue;
168
363
  }
364
+ output.dataDependencies.dependencies.push({
365
+ /**
366
+ * This referenceId is a hash of the property type and the requested properties.
367
+ * This should allow us to create the same hash at execution time, to find the correct data from BE.
368
+ */
369
+ referenceId: (0, utils_1.generateHash)(propertyType, requestedProperties),
370
+ properties: {
371
+ type: propertyType,
372
+ recordProperties: requestedProperties,
373
+ options,
374
+ },
375
+ });
169
376
  }
170
377
  }
171
378
  }
@@ -184,6 +391,7 @@ function traverseAbstractSyntaxTree(ast, checks, extensionPath, logger) {
184
391
  importedHooks: {},
185
392
  dependencies: [],
186
393
  },
394
+ variableDeclarations: new Map(),
187
395
  };
188
396
  try {
189
397
  (0, estraverse_1.traverse)(ast, {
@@ -193,6 +401,7 @@ function traverseAbstractSyntaxTree(ast, checks, extensionPath, logger) {
193
401
  _checkForFunctionMetadata(node, parent, state, check.functionName);
194
402
  });
195
403
  checkForOutOfBoundsImageImports(node, state, extensionPath);
404
+ _collectVariableDeclarations(node, state);
196
405
  _collectDataDependencies(node, state, logger);
197
406
  }
198
407
  catch (e) {
@@ -18,6 +18,7 @@ const codeBlockingPlugin = ({ logger, extensionPath }) => {
18
18
  importedHooks: {},
19
19
  dependencies: [],
20
20
  },
21
+ variableDeclarations: new Map(),
21
22
  };
22
23
  const requireFunctionName = 'require';
23
24
  try {
@@ -1,6 +1,5 @@
1
1
  import { PLATFORM_VERSION, PRIVATE_APP, PUBLIC_APP } from './constants';
2
2
  import { LocalDevUrlMapping } from '@hubspot/app-functions-dev-server';
3
- import { PromptModule } from 'inquirer';
4
3
  export interface ObjectTypes {
5
4
  name: string;
6
5
  }
@@ -203,8 +202,15 @@ export type DataDependency = {
203
202
  properties: {
204
203
  type: string;
205
204
  recordProperties: string[];
205
+ options: {
206
+ [key: string]: any;
207
+ };
206
208
  };
207
209
  };
210
+ export type NodeValue = string | number | boolean | null | undefined | RegExp | bigint | symbol | NodeValue[] | NodeObject;
211
+ export type NodeObject = {
212
+ [key: string]: NodeValue;
213
+ };
208
214
  export interface SourceCodeMetadata {
209
215
  functions: {
210
216
  [functionName: string]: FunctionMetadata;
@@ -216,6 +222,7 @@ export interface SourceCodeMetadata {
216
222
  };
217
223
  dependencies: DataDependency[];
218
224
  };
225
+ variableDeclarations: Map<string, NodeValue>;
219
226
  }
220
227
  export interface FunctionInvocationCheck {
221
228
  functionName: string;
@@ -243,7 +250,6 @@ export interface DevModeStartArguments {
243
250
  }
244
251
  export interface DevModeBaseSetupArguments {
245
252
  onUploadRequired?: VoidFunction;
246
- promptUser: PromptModule;
247
253
  logger: Logger;
248
254
  urls: {
249
255
  api: string;
@@ -16,6 +16,7 @@ export declare function extractAllowedUrls(appConfig?: AppConfig): string[];
16
16
  /**
17
17
  * This function generates a deterministic hash from any number of arguments
18
18
  * Arrays and objects are stringified to ensure it works for all types.
19
+ * Uses the same simple hash algorithm as the browser version for consistency.
19
20
  */
20
21
  export declare function generateHash(...args: unknown[]): string;
21
22
  /**
package/dist/lib/utils.js CHANGED
@@ -6,7 +6,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.isImage = exports.generateHash = exports.extractAllowedUrls = exports.throwUnhandledPlatformVersionError = exports.UnhandledPlatformVersionError = exports.isExtensionFile = exports.isNodeModule = exports.buildSourceId = exports.loadManifest = exports.stripAnsiColorCodes = exports.getUrlSafeFileName = void 0;
7
7
  const path_1 = __importDefault(require("path"));
8
8
  const fs_1 = __importDefault(require("fs"));
9
- const crypto_1 = require("crypto");
10
9
  const constants_1 = require("./constants");
11
10
  function getUrlSafeFileName(filePath) {
12
11
  const { name } = path_1.default.parse(filePath);
@@ -86,9 +85,18 @@ function extractAllowedUrls(appConfig) {
86
85
  return appConfig.allowedUrls;
87
86
  }
88
87
  exports.extractAllowedUrls = extractAllowedUrls;
88
+ function simpleHash(input) {
89
+ let hash = 0;
90
+ for (let i = 0; i < input.length; i++) {
91
+ const char = input.charCodeAt(i);
92
+ hash = (hash * 31 + char) % 2147483647;
93
+ }
94
+ return Math.abs(hash).toString(16);
95
+ }
89
96
  /**
90
97
  * This function generates a deterministic hash from any number of arguments
91
98
  * Arrays and objects are stringified to ensure it works for all types.
99
+ * Uses the same simple hash algorithm as the browser version for consistency.
92
100
  */
93
101
  function generateHash(...args) {
94
102
  try {
@@ -102,9 +110,9 @@ function generateHash(...args) {
102
110
  }
103
111
  return String(arg);
104
112
  });
113
+ // Return the hash of the joined strings.
105
114
  const input = [...normalizedArgs].join('::');
106
- // Return the hash of the joined string.
107
- return (0, crypto_1.createHash)('md5').update(input).digest('hex');
115
+ return simpleHash(input);
108
116
  }
109
117
  catch (e) {
110
118
  console.error('Error generating hash: ', e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/ui-extensions-dev-server",
3
- "version": "0.9.2",
3
+ "version": "0.9.4",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "uie": "./dist/lib/bin/cli.js"
@@ -70,5 +70,5 @@
70
70
  "optional": true
71
71
  }
72
72
  },
73
- "gitHead": "931c7cf7cc3f87a54909e67a0a7df74e4eee2c3b"
73
+ "gitHead": "65046f5f3ce4c51652bc454e4425b57bd33aedbf"
74
74
  }