@hubspot/ui-extensions-dev-server 0.8.41 → 0.8.43

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/lib/ast.d.ts CHANGED
@@ -1,6 +1,12 @@
1
- import { SourceCodeChecks } from './types';
2
- import { Program } from 'estree';
3
- export declare function traverseAbstractSyntaxTree(ast: Program, checks: SourceCodeChecks, extensionPath: string): {
1
+ import { SourceCodeMetadata, SourceCodeChecks, Logger } from './types';
2
+ import { Program, Node } from 'estree';
3
+ /**
4
+ * We only support image imports that are within the extension directory.
5
+ * 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.
6
+ */
7
+ export declare function checkForOutOfBoundsImageImports(node: Node, output: SourceCodeMetadata, extensionPath: string): void;
8
+ export declare function traverseAbstractSyntaxTree(ast: Program, checks: SourceCodeChecks, extensionPath: string, logger: Logger): {
4
9
  functions: {};
5
10
  badImports: never[];
11
+ dataDependencies: never[];
6
12
  };
package/dist/lib/ast.js CHANGED
@@ -4,10 +4,11 @@ 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 = void 0;
7
+ exports.traverseAbstractSyntaxTree = exports.checkForOutOfBoundsImageImports = void 0;
8
8
  const path_1 = __importDefault(require("path"));
9
9
  // @ts-expect-error no type defs
10
10
  const estraverse_1 = require("estraverse");
11
+ const utils_1 = require("./utils");
11
12
  function _isVariableImported(node, variableName) {
12
13
  if (!node) {
13
14
  return false;
@@ -52,37 +53,106 @@ function _checkForFunctionMetadata(node, parent, output, functionName) {
52
53
  }
53
54
  }
54
55
  /**
55
- * We only support imports that are within the extension directory.
56
- * This function will check if an import is out of bounds and collect any that are out of bounds, so we can warn the user before they run into build issues.
56
+ * We only support image imports that are within the extension directory.
57
+ * 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.
57
58
  */
58
- function _checkForOutOfBoundsImports(node, output, extensionPath) {
59
+ function checkForOutOfBoundsImageImports(node, output, extensionPath) {
59
60
  if (!node) {
60
61
  return;
61
62
  }
62
63
  if (node.type === 'ImportDeclaration' &&
63
64
  typeof node.source.value === 'string') {
64
- const importPath = path_1.default.join(extensionPath, node.source.value);
65
- const { dir } = path_1.default.parse(importPath);
66
- if (!dir.includes(extensionPath)) {
65
+ const importPath = node.source.value;
66
+ // Only do the check for images.
67
+ if (!(0, utils_1.isImage)(importPath)) {
68
+ return;
69
+ }
70
+ // Build the full path to the import, using the extension path as the base.
71
+ const absoluteImportPath = path_1.default.resolve(extensionPath, importPath);
72
+ if (!(0, utils_1.isExtensionFile)(absoluteImportPath, extensionPath)) {
67
73
  output.badImports.push(importPath);
68
74
  }
69
75
  }
70
76
  }
77
+ exports.checkForOutOfBoundsImageImports = checkForOutOfBoundsImageImports;
78
+ /**
79
+ * This function collects all internal data dependencies for the extension
80
+ * Specifically, it collects dependencies which are using our custom hooks, eg `useCrmProperties`
81
+ */
82
+ function _collectDataDependencies(node, output, logger) {
83
+ if (!node) {
84
+ return;
85
+ }
86
+ try {
87
+ // For now we only support `useCrmProperties`, but we can generalize this in the future.
88
+ if (node.type === 'CallExpression' &&
89
+ node.callee.type === 'Identifier' &&
90
+ node.callee.name === 'useCrmProperties') {
91
+ const propertyType = 'CrmRecordDataProperties';
92
+ // Get the second argument, the properties array.
93
+ const propertiesNode = node.arguments[1];
94
+ const requestedProperties = [];
95
+ // If the second argument is an array with at least one element, we will collect the properties.
96
+ if (propertiesNode &&
97
+ propertiesNode.type === 'ArrayExpression' &&
98
+ propertiesNode.elements.length > 0) {
99
+ propertiesNode.elements.forEach((element) => {
100
+ /**
101
+ * We only support strings for now, and ignore the rest.
102
+ * Again, this might be more generalized in the future as we support more hooks.
103
+ */
104
+ if (element &&
105
+ element.type === 'Literal' &&
106
+ typeof element.value === 'string') {
107
+ requestedProperties.push(element.value);
108
+ }
109
+ });
110
+ output.dataDependencies.push({
111
+ /**
112
+ * This refID is a hash of the property type and the requested properties.
113
+ * This should allow us to create the same hash at execution time, to find the correct data from BE.
114
+ */
115
+ refId: (0, utils_1.generateHash)(propertyType, requestedProperties),
116
+ properties: {
117
+ type: propertyType,
118
+ recordProperties: requestedProperties,
119
+ },
120
+ });
121
+ }
122
+ }
123
+ }
124
+ catch (e) {
125
+ logger.warn(`Error collecting data dependencies (skipping): ${e instanceof Error ? e.message : String(e)}`);
126
+ }
127
+ }
71
128
  // Traverses an ESTree as defined by the EsTree spec https://github.com/estree/estree
72
129
  // Uses the checks array to search the source code for matches
73
- function traverseAbstractSyntaxTree(ast, checks, extensionPath) {
130
+ function traverseAbstractSyntaxTree(ast, checks, extensionPath, logger) {
74
131
  const state = {
75
132
  functions: {},
76
133
  badImports: [],
134
+ dataDependencies: [],
77
135
  };
78
- (0, estraverse_1.traverse)(ast, {
79
- enter(node, parent) {
80
- checks.forEach((check) => {
81
- _checkForFunctionMetadata(node, parent, state, check.functionName);
82
- _checkForOutOfBoundsImports(node, state, extensionPath);
83
- });
84
- },
85
- });
136
+ try {
137
+ (0, estraverse_1.traverse)(ast, {
138
+ enter(node, parent) {
139
+ try {
140
+ checks.forEach((check) => {
141
+ _checkForFunctionMetadata(node, parent, state, check.functionName);
142
+ });
143
+ checkForOutOfBoundsImageImports(node, state, extensionPath);
144
+ _collectDataDependencies(node, state, logger);
145
+ }
146
+ catch (e) {
147
+ // Don't let individual node processing errors crash the entire traverse.
148
+ logger.warn(`Error processing node: ${JSON.stringify(node)}: ${e instanceof Error ? e.message : String(e)}`);
149
+ }
150
+ },
151
+ });
152
+ }
153
+ catch (e) {
154
+ logger.warn(`Unable to traverse AST: ${e instanceof Error ? e.message : String(e)}`);
155
+ }
86
156
  return state;
87
157
  }
88
158
  exports.traverseAbstractSyntaxTree = traverseAbstractSyntaxTree;
@@ -14,6 +14,7 @@ const codeBlockingPlugin = ({ logger, extensionPath }) => {
14
14
  let sourceCodeMetadata = {
15
15
  functions: {},
16
16
  badImports: [],
17
+ dataDependencies: [],
17
18
  };
18
19
  const requireFunctionName = 'require';
19
20
  try {
@@ -21,7 +22,7 @@ const codeBlockingPlugin = ({ logger, extensionPath }) => {
21
22
  // the docs over on rollup's site specify ESTree.Program as the return type,
22
23
  // and the underlying data matches that https://rollupjs.org/plugin-development/#this-parse
23
24
  const abstractSyntaxTree = this.parse(code);
24
- sourceCodeMetadata = (0, ast_1.traverseAbstractSyntaxTree)(abstractSyntaxTree, [{ functionName: requireFunctionName }], extensionPath);
25
+ sourceCodeMetadata = (0, ast_1.traverseAbstractSyntaxTree)(abstractSyntaxTree, [{ functionName: requireFunctionName }], extensionPath, logger);
25
26
  if (sourceCodeMetadata.badImports) {
26
27
  for (const badImport of sourceCodeMetadata.badImports) {
27
28
  logger.warn(`Importing files from outside of the extension directory is not supported. Please move the import ${badImport} into the extension directory.`);
@@ -10,18 +10,45 @@ const path_1 = require("path");
10
10
  const constants_1 = require("../constants");
11
11
  const path_2 = __importDefault(require("path"));
12
12
  const utils_1 = require("../utils");
13
+ const ast_1 = require("../ast");
13
14
  const PACKAGE_LOCK_FILE = 'package-lock.json';
14
15
  const PACKAGE_FILE = 'package.json';
15
16
  const EXTENSIONS_PATH = 'src/app/extensions/';
16
17
  const manifestPlugin = (options) => {
18
+ let allDataDependencies;
17
19
  return {
18
20
  name: 'ui-extensions-manifest-generation-plugin',
19
21
  enforce: 'post',
22
+ buildStart() {
23
+ // Reset the source metadata for the new build
24
+ allDataDependencies = [];
25
+ },
26
+ transform(code, filename) {
27
+ const { logger } = options;
28
+ // We only need to parse the AST for extension files, so check that first.
29
+ const isExtension = (0, utils_1.isExtensionFile)(filename, options.extensionPath || '');
30
+ if (!isExtension) {
31
+ return { code, map: null };
32
+ }
33
+ try {
34
+ const ast = this.parse(code);
35
+ const sourceCodeMetadata = (0, ast_1.traverseAbstractSyntaxTree)(ast, [], options.extensionPath || process.cwd(), logger);
36
+ allDataDependencies = [
37
+ ...allDataDependencies,
38
+ ...sourceCodeMetadata.dataDependencies,
39
+ ];
40
+ }
41
+ catch (e) {
42
+ logger.error(`Unable to parse source code for ${filename}, ${e}`);
43
+ }
44
+ // We don't need to actually transform anything, just collect data, so return the original code
45
+ return { code, map: null };
46
+ },
20
47
  generateBundle(_rollupOptions, bundle) {
21
48
  const { output, minify = false, extensionPath = process.cwd(), logger, } = options;
22
49
  try {
23
50
  const filename = path_2.default.parse(output).name;
24
- const manifest = _generateManifestContents(bundle, extensionPath);
51
+ const manifest = _generateManifestContents(bundle, extensionPath, allDataDependencies);
25
52
  this.emitFile({
26
53
  type: 'asset',
27
54
  source: minify
@@ -36,19 +63,22 @@ const manifestPlugin = (options) => {
36
63
  },
37
64
  };
38
65
  };
39
- function _generateManifestContents(bundle, extensionPath) {
66
+ function _generateManifestContents(bundle, extensionPath, allDataDependencies) {
40
67
  const baseManifest = {
41
68
  package: _loadPackageFile(extensionPath),
42
69
  };
70
+ const dataDependencies = {
71
+ dataDeps: allDataDependencies !== null && allDataDependencies !== void 0 ? allDataDependencies : [],
72
+ };
43
73
  // The keys to bundle are the filename without any path information
44
74
  const bundles = Object.keys(bundle).filter((cur) => cur.endsWith('.js'));
45
75
  if (bundles.length === 1) {
46
- return Object.assign(Object.assign({}, _generateManifestEntry(bundle[bundles[0]])), baseManifest);
76
+ return Object.assign(Object.assign(Object.assign({}, _generateManifestEntry(bundle[bundles[0]])), dataDependencies), baseManifest);
47
77
  }
48
78
  const manifest = bundles.reduce((acc, current) => {
49
79
  return Object.assign(Object.assign({}, acc), { [current]: _generateManifestEntry(bundle[current]) });
50
80
  }, {});
51
- return Object.assign(Object.assign({}, manifest), baseManifest);
81
+ return Object.assign(Object.assign(Object.assign({}, manifest), dataDependencies), baseManifest);
52
82
  }
53
83
  function _generateManifestEntry(subBundle) {
54
84
  const { facadeModuleId, moduleIds, modules } = subBundle;
@@ -115,11 +115,19 @@ export interface FunctionMetadata {
115
115
  defined?: boolean;
116
116
  invoked?: boolean;
117
117
  }
118
+ export type DataDependency = {
119
+ refId: string;
120
+ properties: {
121
+ type: string;
122
+ recordProperties: string[];
123
+ };
124
+ };
118
125
  export interface SourceCodeMetadata {
119
126
  functions: {
120
127
  [functionName: string]: FunctionMetadata;
121
128
  };
122
129
  badImports: string[];
130
+ dataDependencies: DataDependency[];
123
131
  }
124
132
  export interface FunctionInvocationCheck {
125
133
  functionName: string;
@@ -4,8 +4,21 @@ export declare function stripAnsiColorCodes(stringWithColorCodes: string | undef
4
4
  export declare function loadManifest(outputDir: string, output: string): any;
5
5
  export declare function buildSourceId(appConfig: AppConfig, extensionConfig: ExtensionConfig): string | null;
6
6
  export declare function isNodeModule(filepath: string | undefined): boolean;
7
+ /**
8
+ * Check if a given file is within the extension path
9
+ */
10
+ export declare function isExtensionFile(filepath: string | undefined, extensionPath: string): boolean;
7
11
  export declare class UnhandledPlatformVersionError extends Error {
8
12
  constructor(platformVersion: never);
9
13
  }
10
14
  export declare function throwUnhandledPlatformVersionError(platformVersion: never): never;
11
15
  export declare function extractAllowedUrls(appConfig?: AppConfig): string[];
16
+ /**
17
+ * This function generates a deterministic hash from any number of arguments
18
+ * Arrays and objects are stringified to ensure it works for all types.
19
+ */
20
+ export declare function generateHash(...args: unknown[]): string;
21
+ /**
22
+ * Check if a given URL is an image (of a type we support)
23
+ */
24
+ export declare function isImage(url: string): boolean;
package/dist/lib/utils.js CHANGED
@@ -3,9 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.extractAllowedUrls = exports.throwUnhandledPlatformVersionError = exports.UnhandledPlatformVersionError = exports.isNodeModule = exports.buildSourceId = exports.loadManifest = exports.stripAnsiColorCodes = exports.getUrlSafeFileName = void 0;
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");
9
10
  const constants_1 = require("./constants");
10
11
  function getUrlSafeFileName(filePath) {
11
12
  const { name } = path_1.default.parse(filePath);
@@ -48,6 +49,19 @@ function isNodeModule(filepath) {
48
49
  return directory.includes('node_modules');
49
50
  }
50
51
  exports.isNodeModule = isNodeModule;
52
+ /**
53
+ * Check if a given file is within the extension path
54
+ */
55
+ function isExtensionFile(filepath, extensionPath) {
56
+ if (!filepath) {
57
+ return false;
58
+ }
59
+ const absoluteFilePath = path_1.default.resolve(filepath).toLowerCase();
60
+ const absoluteExtensionDirPath = path_1.default.resolve(extensionPath).toLowerCase();
61
+ const relativePath = path_1.default.relative(absoluteExtensionDirPath, absoluteFilePath);
62
+ return !relativePath.startsWith('..');
63
+ }
64
+ exports.isExtensionFile = isExtensionFile;
51
65
  class UnhandledPlatformVersionError extends Error {
52
66
  constructor(platformVersion) {
53
67
  super(`Unsupported platform version "${platformVersion}"`);
@@ -65,3 +79,37 @@ function extractAllowedUrls(appConfig) {
65
79
  return appConfig.allowedUrls;
66
80
  }
67
81
  exports.extractAllowedUrls = extractAllowedUrls;
82
+ /**
83
+ * This function generates a deterministic hash from any number of arguments
84
+ * Arrays and objects are stringified to ensure it works for all types.
85
+ */
86
+ function generateHash(...args) {
87
+ try {
88
+ // First make sure all the values are strings
89
+ const normalizedArgs = args.map((arg) => {
90
+ if (Array.isArray(arg)) {
91
+ return [...arg].sort().join('-');
92
+ }
93
+ if (arg && typeof arg === 'object') {
94
+ return JSON.stringify(arg, Object.keys(arg).sort());
95
+ }
96
+ return String(arg);
97
+ });
98
+ const input = [...normalizedArgs].join('::');
99
+ // Return the hash of the joined string.
100
+ return (0, crypto_1.createHash)('md5').update(input).digest('hex');
101
+ }
102
+ catch (e) {
103
+ console.error('Error generating hash: ', e);
104
+ // Just return an empty string if anything goes wrong.
105
+ return '';
106
+ }
107
+ }
108
+ exports.generateHash = generateHash;
109
+ /**
110
+ * Check if a given URL is an image (of a type we support)
111
+ */
112
+ function isImage(url) {
113
+ return /\.(png|jpg|jpeg|gif|svg|webp|avif|raw|url|inline)$/.test(url);
114
+ }
115
+ exports.isImage = isImage;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/ui-extensions-dev-server",
3
- "version": "0.8.41",
3
+ "version": "0.8.43",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "uie": "./dist/lib/bin/cli.js"
@@ -27,16 +27,16 @@
27
27
  ],
28
28
  "license": "MIT",
29
29
  "dependencies": {
30
- "@hubspot/app-functions-dev-server": "0.8.41",
31
- "chalk": "^5.4.1",
32
- "commander": "^13.0.0",
33
- "cors": "^2.8.5",
30
+ "@hubspot/app-functions-dev-server": "0.8.43",
31
+ "chalk": "5.4.1",
32
+ "commander": "13.0.0",
33
+ "cors": "2.8.5",
34
34
  "detect-port": "1.5.1",
35
- "estraverse": "^5.3.0",
36
- "express": "^4.18.2",
35
+ "estraverse": "5.3.0",
36
+ "express": "4.18.2",
37
37
  "inquirer": "8.2.0",
38
- "ora": "^8.1.1",
39
- "vite": "^4.4.9"
38
+ "ora": "8.1.1",
39
+ "vite": "4.5.5"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/estree": "^1.0.5",
@@ -45,7 +45,7 @@
45
45
  "@types/jest": "^29.5.4",
46
46
  "acorn": "^8.11.2",
47
47
  "ava": "4.3.3",
48
- "axios": "^1.6.8",
48
+ "axios": "1.6.8",
49
49
  "jest": "^29.5.0",
50
50
  "ts-jest": "^29.1.1",
51
51
  "typescript": "^5.1.6",
@@ -69,5 +69,5 @@
69
69
  "optional": true
70
70
  }
71
71
  },
72
- "gitHead": "ce6d6332d471d5c5ac127de88a8fa51dd7506c3e"
72
+ "gitHead": "a140eea0a991c41c86030a5017b54bfd6151ab4c"
73
73
  }