@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,152 @@
1
+ import { afterEach, describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import * as devServer from '@hubspot/app-functions-dev-server';
3
+ import startDevServer from "../server.js";
4
+ import { DevServerState } from "../DevServerState.js";
5
+ import { createMockLogger, createDevServerConfig, createMockViteDevServer, } from "./factories.js";
6
+ const useMock = vi.fn();
7
+ vi.mock('@hubspot/app-functions-dev-server', () => ({
8
+ AppProxyService: vi.fn(() => vi.fn((req, res, next) => next())),
9
+ AppFunctionExecutionService: vi.fn(() => vi.fn((req, res, next) => next())),
10
+ }));
11
+ vi.mock('express', () => {
12
+ const expressMock = () => ({
13
+ use: useMock,
14
+ get: vi.fn(),
15
+ listen: (__, callback) => {
16
+ // simulate async listen
17
+ const serverMock = {
18
+ on: vi.fn(function (event) {
19
+ if (event === 'error') {
20
+ // Don't call error handler in tests
21
+ }
22
+ return this;
23
+ }),
24
+ close: vi.fn((cb) => cb && cb()),
25
+ };
26
+ setTimeout(callback, 1);
27
+ return serverMock;
28
+ },
29
+ });
30
+ expressMock.static = vi.fn(() => vi.fn());
31
+ return { default: expressMock };
32
+ });
33
+ describe('server', () => {
34
+ let logger;
35
+ let devServerConfig;
36
+ beforeEach(() => {
37
+ logger = createMockLogger();
38
+ devServerConfig = createDevServerConfig(logger);
39
+ });
40
+ afterEach(() => {
41
+ vi.resetAllMocks();
42
+ });
43
+ describe('startDevServer', () => {
44
+ beforeEach(() => {
45
+ useMock.mockClear();
46
+ vi.mocked(devServer.AppFunctionExecutionService).mockReturnValue(vi.fn());
47
+ vi.mocked(devServer.AppProxyService).mockReturnValue(vi.fn());
48
+ vi.clearAllMocks();
49
+ });
50
+ it('should add the AppProxyService middleware when localDevUrlMapping is provided', async () => {
51
+ const devServerState = new DevServerState({
52
+ ...devServerConfig,
53
+ localDevUrlMapping: {
54
+ 'https://inbound.com': 'http://localhost',
55
+ },
56
+ });
57
+ await startDevServer({
58
+ devServerState,
59
+ viteDevServer: createMockViteDevServer(),
60
+ });
61
+ expect(useMock).toHaveBeenNthCalledWith(3, '/api/crm-extensibility/execution/internal/v3', expect.any(Function),
62
+ // This is the AppProxyService middleware
63
+ expect.any(Function));
64
+ expect(devServer.AppProxyService).toHaveBeenCalledTimes(1);
65
+ expect(devServer.AppProxyService).toHaveBeenCalledWith({
66
+ accountId: devServerConfig.accountId,
67
+ allowedUrls: [],
68
+ localDevUrlMapping: devServerState.localDevUrlMapping,
69
+ logger: devServerState.logger,
70
+ });
71
+ });
72
+ it('should not add the AppProxyService middleware when the localDevUrlMapping is empty', async () => {
73
+ const devServerState = new DevServerState({
74
+ ...devServerConfig,
75
+ localDevUrlMapping: {},
76
+ });
77
+ await startDevServer({
78
+ devServerState,
79
+ viteDevServer: createMockViteDevServer(),
80
+ });
81
+ expect(useMock).toHaveBeenNthCalledWith(3, '/api/crm-extensibility/execution/internal/v3', expect.any(Function));
82
+ expect(devServer.AppProxyService).toHaveBeenCalledTimes(0);
83
+ });
84
+ it('should not add the AppProxyService middleware when localDevUrlMapping is not provided', async () => {
85
+ const devServerState = new DevServerState({
86
+ ...devServerConfig,
87
+ localDevUrlMapping: undefined,
88
+ });
89
+ await startDevServer({
90
+ devServerState,
91
+ viteDevServer: createMockViteDevServer(),
92
+ });
93
+ expect(useMock).toHaveBeenNthCalledWith(3, '/api/crm-extensibility/execution/internal/v3', expect.any(Function));
94
+ expect(devServer.AppProxyService).toHaveBeenCalledTimes(0);
95
+ });
96
+ it('should add the AppFunctionExecutionService middleware if is a private app', async () => {
97
+ const devServerState = new DevServerState({
98
+ ...devServerConfig,
99
+ localDevUrlMapping: {
100
+ 'https://inbound.com': 'http://localhost',
101
+ },
102
+ });
103
+ await startDevServer({
104
+ devServerState,
105
+ viteDevServer: createMockViteDevServer(),
106
+ });
107
+ expect(devServer.AppFunctionExecutionService).toHaveBeenCalledTimes(1);
108
+ });
109
+ it('should not add the AppFunctionExecutionService middleware if is a public app', async () => {
110
+ const devServerState = new DevServerState({
111
+ ...devServerConfig,
112
+ appConfig: { isPublicApp: true },
113
+ localDevUrlMapping: {
114
+ 'https://inbound.com': 'http://localhost',
115
+ },
116
+ });
117
+ await startDevServer({
118
+ devServerState,
119
+ viteDevServer: createMockViteDevServer(),
120
+ });
121
+ expect(devServer.AppFunctionExecutionService).not.toHaveBeenCalled();
122
+ });
123
+ it('should return httpServer and shutdown function', async () => {
124
+ const devServerState = new DevServerState(devServerConfig);
125
+ const result = await startDevServer({
126
+ devServerState,
127
+ viteDevServer: createMockViteDevServer(),
128
+ });
129
+ expect(result).toHaveProperty('httpServer');
130
+ expect(result).toHaveProperty('shutdown');
131
+ expect(typeof result.shutdown).toBe('function');
132
+ });
133
+ it('should initialize ExtensionsWebSocket on devServerState', async () => {
134
+ const devServerState = new DevServerState(devServerConfig);
135
+ await startDevServer({
136
+ devServerState,
137
+ viteDevServer: createMockViteDevServer(),
138
+ });
139
+ expect(devServerState.extensionsWebSocket).toBeDefined();
140
+ });
141
+ it('should trigger WebSocket setup after initialization', async () => {
142
+ const triggerSpy = vi.spyOn(DevServerState.prototype, 'triggerWebSocketSetup');
143
+ const devServerState = new DevServerState(devServerConfig);
144
+ await startDevServer({
145
+ devServerState,
146
+ viteDevServer: createMockViteDevServer(),
147
+ });
148
+ expect(triggerSpy).toHaveBeenCalledTimes(1);
149
+ triggerSpy.mockRestore();
150
+ });
151
+ });
152
+ });
@@ -0,0 +1 @@
1
+ export declare const localParse: (code: string) => import("acorn").Program;
@@ -0,0 +1,4 @@
1
+ import { parse } from 'acorn';
2
+ export const localParse = (code) => {
3
+ return parse(code, { ecmaVersion: 'latest', sourceType: 'module' });
4
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,176 @@
1
+ import { describe, it, expect, vi, beforeAll, afterAll, beforeEach, afterEach, } from 'vitest';
2
+ import { OUTPUT_DIR } from "../constants.js";
3
+ import { getUrlSafeFileName, stripAnsiColorCodes, loadManifest, buildSourceId, isNodeModule, isExtensionFile, generateHash, } from "../utils.js";
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ describe('utils', () => {
7
+ describe('getUrlSafeFileName', () => {
8
+ it('should uri encode the filename', () => {
9
+ const fileName = 'test File ?@#$%^&*()Name who would name a file like this!';
10
+ const actual = getUrlSafeFileName(`${fileName}.tsx`);
11
+ expect(actual).toEqual(`${encodeURIComponent(fileName)}.js`);
12
+ });
13
+ });
14
+ describe('stripAnsiColorCodes', () => {
15
+ it('should strip all color codes out of a string', () => {
16
+ const withColorCodes = '\x1B[33mThis is a test\x1B[39m';
17
+ const actual = stripAnsiColorCodes(withColorCodes);
18
+ expect(actual).toEqual('This is a test');
19
+ });
20
+ it('should return null if the provided string is null', () => {
21
+ const actual = stripAnsiColorCodes(undefined);
22
+ expect(actual).toEqual(null);
23
+ });
24
+ it('should return null if the provided string is undefined', () => {
25
+ const actual = stripAnsiColorCodes(undefined);
26
+ expect(actual).toEqual(null);
27
+ });
28
+ it('should return null if the provided string is empty', () => {
29
+ const actual = stripAnsiColorCodes('');
30
+ expect(actual).toEqual(null);
31
+ });
32
+ });
33
+ describe('loadManifest', () => {
34
+ beforeEach(() => {
35
+ vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify({
36
+ manifest: true,
37
+ }));
38
+ });
39
+ afterEach(() => {
40
+ vi.restoreAllMocks();
41
+ });
42
+ it('should return the manifest file when it is able to load it from disk', () => {
43
+ const actual = loadManifest(OUTPUT_DIR, 'test-file-name');
44
+ expect(fs.readFileSync).toHaveBeenCalledTimes(1);
45
+ expect(actual).toStrictEqual({
46
+ manifest: true,
47
+ });
48
+ });
49
+ it('should return an empty object when it fails to load from disk', () => {
50
+ vi.spyOn(fs, 'readFileSync').mockImplementationOnce(() => {
51
+ throw new Error('OH NO!');
52
+ });
53
+ const actual = loadManifest(OUTPUT_DIR, 'test-file-name');
54
+ expect(actual).toStrictEqual({});
55
+ });
56
+ });
57
+ describe('buildSourceId', () => {
58
+ it('should return null when there is no uid in app config', () => {
59
+ expect(buildSourceId({}, { data: { uid: 'some-uid' } })).toBe(null);
60
+ });
61
+ it('should return null when there is no uid in extension config', () => {
62
+ expect(buildSourceId({ uid: 'some-uid' }, { data: {} })).toBe(null);
63
+ });
64
+ it('should return a valid sourceId', () => {
65
+ const appUID = 'app-uid';
66
+ const extensionUID = 'extension-uid';
67
+ expect(buildSourceId({ uid: appUID }, { data: { uid: extensionUID } })).toBe(`${appUID}::${extensionUID}`);
68
+ });
69
+ });
70
+ describe('isNodeModule', () => {
71
+ it('should return false if the path is undefined', () => {
72
+ expect(isNodeModule(undefined)).toBe(false);
73
+ });
74
+ it('should return false if the path is not a node_modules', () => {
75
+ expect(isNodeModule('foo.js')).toBe(false);
76
+ });
77
+ it('should return true if the path is a node_modules', () => {
78
+ expect(isNodeModule('node_modules/foo.js')).toBe(true);
79
+ });
80
+ });
81
+ describe('isExtensionFile', () => {
82
+ /**
83
+ * This function relies on the file system. Mocking doesn't allow us to fully test all scenarios.
84
+ * In order to effectively test it, we need to create a temporary directory structure.
85
+ * Being thorough here allows us to safely mock it elsewhere.
86
+ */
87
+ const tempDir = path.join(__dirname, 'temp');
88
+ const extensionDir = path.join(tempDir, 'test/path/extension');
89
+ const indexFile = path.join(extensionDir, 'index.js');
90
+ const nestedFile = path.join(extensionDir, 'src/file.ts');
91
+ const otherFile = path.join(tempDir, 'different/path/file.ts');
92
+ const relativeOtherFile = path.join(tempDir, '../different/path/file.ts');
93
+ beforeAll(() => {
94
+ fs.mkdirSync(extensionDir, { recursive: true });
95
+ fs.mkdirSync(path.dirname(nestedFile), { recursive: true });
96
+ fs.mkdirSync(path.dirname(otherFile), { recursive: true });
97
+ fs.mkdirSync(path.dirname(relativeOtherFile), { recursive: true });
98
+ fs.writeFileSync(otherFile, '');
99
+ fs.writeFileSync(nestedFile, '');
100
+ fs.writeFileSync(indexFile, '');
101
+ fs.writeFileSync(relativeOtherFile, '');
102
+ });
103
+ afterAll(() => {
104
+ fs.rmSync(tempDir, { recursive: true, force: true });
105
+ fs.rmSync(relativeOtherFile, { recursive: true, force: true });
106
+ });
107
+ it('should return false if filepath is undefined', () => {
108
+ expect(isExtensionFile(undefined, extensionDir)).toBe(false);
109
+ });
110
+ it('should return true if file is within extension path', () => {
111
+ expect(isExtensionFile(nestedFile, extensionDir)).toBe(true);
112
+ expect(isExtensionFile(indexFile, extensionDir)).toBe(true);
113
+ });
114
+ it('should return false if file is outside extension path', () => {
115
+ expect(isExtensionFile(otherFile, extensionDir)).toBe(false);
116
+ });
117
+ it('should handle relative paths correctly', () => {
118
+ const originalCwd = process.cwd();
119
+ process.chdir(tempDir);
120
+ expect(isExtensionFile('./test/path/extension/src/file.ts', './test/path/extension')).toBe(true);
121
+ expect(isExtensionFile('../different/path/file.ts', './test/path/extension')).toBe(false);
122
+ process.chdir(originalCwd);
123
+ });
124
+ it('should return true if the extension path is nested within the provided path', () => {
125
+ const originalCwd = process.cwd();
126
+ process.chdir(tempDir);
127
+ expect(isExtensionFile('../temp/test/path/extension/src/file.ts', './test/path/extension/')).toBe(true);
128
+ process.chdir(originalCwd);
129
+ });
130
+ });
131
+ describe('generateHash', () => {
132
+ it('should generate consistent hashes for same inputs', () => {
133
+ const hash1 = generateHash('test', ['a', 'b'], { foo: 'bar' });
134
+ const hash2 = generateHash('test', ['a', 'b'], { foo: 'bar' });
135
+ expect(hash1).toBe(hash2);
136
+ });
137
+ it('should handle different types of arguments', () => {
138
+ expect(generateHash('test')).toBe('364492');
139
+ expect(generateHash(123)).toBe('be32');
140
+ expect(generateHash(['a', 'b'])).toBe('171f6');
141
+ expect(generateHash({ foo: 'bar' })).toBe('4a4201ac');
142
+ expect(generateHash(null)).toBe('33c587');
143
+ expect(generateHash(undefined)).toBe('42201c7f');
144
+ });
145
+ it('should generate the correct hash for multiple arguments', () => {
146
+ const hash1 = generateHash('arg1', ['foo', 'bar']);
147
+ const hash2 = generateHash('test', ['a', 'c', 'd', 'e']);
148
+ expect(hash1).toBe('1ec304f1');
149
+ expect(hash2).toBe('607ad458');
150
+ });
151
+ it('should normalize arrays regardless of order', () => {
152
+ const hash1 = generateHash(['a', 'b', 'c']);
153
+ const hash2 = generateHash(['c', 'a', 'b']);
154
+ expect(hash1).toBe(hash2);
155
+ });
156
+ it('should normalize objects regardless of key order', () => {
157
+ const hash1 = generateHash({ a: 1, b: 2 });
158
+ const hash2 = generateHash({ b: 2, a: 1 });
159
+ expect(hash1).toBe(hash2);
160
+ });
161
+ it('should return an empty string and log an error if an error is thrown', () => {
162
+ // Mock console.error to prevent it from logging to the console
163
+ const consoleErrorSpy = vi
164
+ .spyOn(console, 'error')
165
+ .mockImplementation(() => { });
166
+ // Create an argument that will cause an error.
167
+ const circularReference = {};
168
+ circularReference.self = circularReference;
169
+ const hash = generateHash(circularReference);
170
+ expect(hash).toBe('');
171
+ expect(consoleErrorSpy).toHaveBeenCalledWith('Error generating hash: ', expect.any(TypeError));
172
+ // Restore console.error
173
+ consoleErrorSpy.mockRestore();
174
+ });
175
+ });
176
+ });
@@ -0,0 +1,16 @@
1
+ import { SourceCodeMetadata, SourceCodeChecks, Logger, NodeValue } from './types.ts';
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): {
9
+ functions: {};
10
+ badImports: never[];
11
+ dataDependencies: {
12
+ importedHooks: {};
13
+ dependencies: never[];
14
+ };
15
+ variableDeclarations: Map<string, NodeValue>;
16
+ };
@@ -0,0 +1,281 @@
1
+ import path from 'path';
2
+ // @ts-expect-error no type defs
3
+ import { traverse } from 'estraverse';
4
+ import { generateHash, isExtensionFile, isImage } from "./utils.js";
5
+ import { getValueFromNode, isFunctionInvoked, isIdentifierDefined, isVariableImported, } from "./parsing-utils.js";
6
+ const PARSED_HOOKS = ['useCrmProperties', 'useAssociations'];
7
+ function _checkForFunctionMetadata(node, parent, output, functionName) {
8
+ if (!node) {
9
+ return;
10
+ }
11
+ if (!output.functions[functionName]) {
12
+ output.functions[functionName] = {};
13
+ }
14
+ if (isFunctionInvoked(node, functionName)) {
15
+ output.functions[functionName].invoked = true;
16
+ // If the function is invoked before being defined we will assume it is a global function
17
+ output.functions[functionName].scope = output.functions[functionName]
18
+ .defined
19
+ ? 'Local'
20
+ : 'Global';
21
+ }
22
+ else if (isIdentifierDefined(node, parent, functionName) ||
23
+ isVariableImported(node, functionName)) {
24
+ output.functions[functionName].defined = true;
25
+ }
26
+ }
27
+ function _collectVariableDeclarations(node, state) {
28
+ if (!node) {
29
+ return;
30
+ }
31
+ // Handle variable declarations (const, let, var)
32
+ if (node.type === 'VariableDeclaration') {
33
+ node.declarations.forEach((declaration) => {
34
+ if (declaration.type === 'VariableDeclarator' &&
35
+ declaration.id.type === 'Identifier' &&
36
+ declaration.init) {
37
+ const variableName = declaration.id.name;
38
+ const result = getValueFromNode(declaration.init, state);
39
+ if (result.status === 'SUCCESS') {
40
+ state.variableDeclarations.set(variableName, result.nodeValue);
41
+ }
42
+ }
43
+ });
44
+ }
45
+ // Handle assignment expressions for let variables (e.g., myVar = newValue)
46
+ if (node.type === 'AssignmentExpression' && node.left.type === 'Identifier') {
47
+ const variableName = node.left.name;
48
+ if (state.variableDeclarations.has(variableName)) {
49
+ const result = getValueFromNode(node.right, state);
50
+ if (result.status === 'SUCCESS') {
51
+ state.variableDeclarations.set(variableName, result.nodeValue);
52
+ }
53
+ }
54
+ }
55
+ }
56
+ /**
57
+ * We only support image imports that are within the extension directory.
58
+ * 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.
59
+ */
60
+ export function checkForOutOfBoundsImageImports(node, output, extensionPath) {
61
+ if (!node) {
62
+ return;
63
+ }
64
+ if (node.type === 'ImportDeclaration' &&
65
+ typeof node.source.value === 'string') {
66
+ const importPath = node.source.value;
67
+ // Only do the check for images.
68
+ if (!isImage(importPath)) {
69
+ return;
70
+ }
71
+ // Build the full path to the import, using the extension path as the base.
72
+ const absoluteImportPath = path.resolve(extensionPath, importPath);
73
+ if (!isExtensionFile(absoluteImportPath, extensionPath)) {
74
+ output.badImports.push(importPath);
75
+ }
76
+ }
77
+ }
78
+ function _processCrmPropertiesHook(node, output) {
79
+ if (node.type !== 'CallExpression')
80
+ return;
81
+ const propertyType = 'CrmRecordProperties';
82
+ const propertiesNode = node.arguments[0];
83
+ const optionsNode = node.arguments[1];
84
+ const requestedProperties = [];
85
+ const propertiesResult = propertiesNode
86
+ ? getValueFromNode(propertiesNode, output)
87
+ : null;
88
+ if (propertiesResult &&
89
+ propertiesResult.status === 'SUCCESS' &&
90
+ Array.isArray(propertiesResult.nodeValue)) {
91
+ propertiesResult.nodeValue.forEach((val) => {
92
+ if (typeof val === 'string') {
93
+ requestedProperties.push(val);
94
+ }
95
+ });
96
+ }
97
+ if (requestedProperties.length > 0) {
98
+ let options = {};
99
+ const optionsResult = optionsNode
100
+ ? getValueFromNode(optionsNode, output)
101
+ : null;
102
+ if (optionsResult &&
103
+ optionsResult.status === 'SUCCESS' &&
104
+ optionsResult.nodeValue &&
105
+ typeof optionsResult.nodeValue === 'object' &&
106
+ !Array.isArray(optionsResult.nodeValue) &&
107
+ !(optionsResult.nodeValue instanceof RegExp)) {
108
+ options = optionsResult.nodeValue;
109
+ }
110
+ output.dataDependencies.dependencies.push({
111
+ referenceId: generateHash(propertyType, requestedProperties),
112
+ properties: {
113
+ type: propertyType,
114
+ recordProperties: requestedProperties,
115
+ options,
116
+ },
117
+ });
118
+ }
119
+ }
120
+ function _processAssociationsHook(node, output) {
121
+ if (node.type !== 'CallExpression')
122
+ return;
123
+ const propertyType = 'CrmRecordAssociationProperties';
124
+ const requestNode = node.arguments[0];
125
+ const optionsNode = node.arguments[1];
126
+ const requestResult = requestNode
127
+ ? getValueFromNode(requestNode, output)
128
+ : null;
129
+ if (!requestResult ||
130
+ requestResult.status === 'FAIL' ||
131
+ !requestResult.nodeValue ||
132
+ typeof requestResult.nodeValue !== 'object' ||
133
+ Array.isArray(requestResult.nodeValue)) {
134
+ return;
135
+ }
136
+ const request = requestResult.nodeValue;
137
+ const toObjectTypeId = request.toObjectType;
138
+ const requestProperties = request.properties;
139
+ if (typeof toObjectTypeId !== 'string') {
140
+ return;
141
+ }
142
+ const propertiesArray = [];
143
+ if (requestProperties && Array.isArray(requestProperties)) {
144
+ requestProperties.forEach((val) => {
145
+ if (typeof val === 'string') {
146
+ propertiesArray.push(val);
147
+ }
148
+ });
149
+ }
150
+ const paginationOptions = {};
151
+ const otherOptions = {};
152
+ const optionsResult = optionsNode
153
+ ? getValueFromNode(optionsNode, output)
154
+ : null;
155
+ if (optionsResult &&
156
+ optionsResult.status === 'SUCCESS' &&
157
+ optionsResult.nodeValue &&
158
+ typeof optionsResult.nodeValue === 'object' &&
159
+ !Array.isArray(optionsResult.nodeValue) &&
160
+ !(optionsResult.nodeValue instanceof RegExp)) {
161
+ const options = optionsResult.nodeValue;
162
+ Object.keys(options).forEach((key) => {
163
+ otherOptions[key] = options[key];
164
+ });
165
+ }
166
+ if (request.pageLength !== undefined) {
167
+ paginationOptions.pageLength = request.pageLength;
168
+ }
169
+ if (request.offset !== undefined) {
170
+ paginationOptions.offset = request.offset;
171
+ }
172
+ const sortedPropertiesForHash = [...propertiesArray].sort();
173
+ output.dataDependencies.dependencies.push({
174
+ referenceId: generateHash(propertyType, toObjectTypeId, sortedPropertiesForHash.join('-')),
175
+ properties: {
176
+ type: propertyType,
177
+ toObjectTypeId,
178
+ requestProperties: propertiesArray,
179
+ paginationOptions,
180
+ options: otherOptions,
181
+ },
182
+ });
183
+ }
184
+ /**
185
+ * This function collects all internal data dependencies for the extension
186
+ * Specifically, it collects dependencies which are using our custom hooks, eg `useCrmProperties`
187
+ */
188
+ function _collectDataDependencies(node, output, logger) {
189
+ if (!node) {
190
+ return;
191
+ }
192
+ try {
193
+ // Check for imports of our hooks.
194
+ if (node.type === 'ImportDeclaration' &&
195
+ typeof node.source.value === 'string' &&
196
+ node.source.value.startsWith('@hubspot/ui-extensions')) {
197
+ // If the imports are coming from our own package, loop over them to check for hooks
198
+ node.specifiers.forEach((specifier) => {
199
+ // If the specifier is an ImportSpecifier and the imported name is one of our tracked hooks, we will track it.
200
+ if (specifier.type === 'ImportSpecifier' &&
201
+ specifier.imported.type === 'Identifier' &&
202
+ PARSED_HOOKS.includes(specifier.imported.name)) {
203
+ // The local name is the name the hook is imported as in the file, and the imported name is the original name of the hook.
204
+ output.dataDependencies.importedHooks[`${specifier.local.name}`] =
205
+ specifier.imported.name;
206
+ }
207
+ else if (
208
+ // We also have to track namespace level imports
209
+ specifier.type === 'ImportNamespaceSpecifier') {
210
+ // If the specifier is a namespace import, we will track all hooks that are imported from that namespace.
211
+ PARSED_HOOKS.forEach((hook) => {
212
+ output.dataDependencies.importedHooks[`${specifier.local.name}.${hook}`] = hook;
213
+ });
214
+ }
215
+ });
216
+ // Check for calls to our hooks.
217
+ }
218
+ else if (node.type === 'CallExpression') {
219
+ let hookName = '';
220
+ // This handles the case where the hook is called directly, eg `useCrmProperties()` (also supports aliased hooks)
221
+ if (node.callee.type === 'Identifier' &&
222
+ output.dataDependencies.importedHooks[node.callee.name]) {
223
+ hookName = output.dataDependencies.importedHooks[node.callee.name];
224
+ }
225
+ else if (
226
+ // This handles namespace import usage, eg `uiExtensions.useCrmProperties()`
227
+ node.callee.type === 'MemberExpression' &&
228
+ node.callee.object.type === 'Identifier' &&
229
+ node.callee.property.type === 'Identifier' &&
230
+ output.dataDependencies.importedHooks[`${node.callee.object.name}.${node.callee.property.name}`]) {
231
+ hookName =
232
+ output.dataDependencies.importedHooks[`${node.callee.object.name}.${node.callee.property.name}`];
233
+ }
234
+ // Then we handle each hook individually, as the usages and tracking format are different.
235
+ if (hookName === 'useCrmProperties') {
236
+ _processCrmPropertiesHook(node, output);
237
+ }
238
+ else if (hookName === 'useAssociations') {
239
+ _processAssociationsHook(node, output);
240
+ }
241
+ }
242
+ }
243
+ catch (e) {
244
+ logger.warn(`Error collecting data dependencies (skipping): ${e instanceof Error ? e.message : String(e)}`);
245
+ }
246
+ }
247
+ // Traverses an ESTree as defined by the EsTree spec https://github.com/estree/estree
248
+ // Uses the checks array to search the source code for matches
249
+ export function traverseAbstractSyntaxTree(ast, checks, extensionPath, logger) {
250
+ const state = {
251
+ functions: {},
252
+ badImports: [],
253
+ dataDependencies: {
254
+ importedHooks: {},
255
+ dependencies: [],
256
+ },
257
+ variableDeclarations: new Map(),
258
+ };
259
+ try {
260
+ traverse(ast, {
261
+ enter(node, parent) {
262
+ try {
263
+ checks.forEach((check) => {
264
+ _checkForFunctionMetadata(node, parent, state, check.functionName);
265
+ });
266
+ checkForOutOfBoundsImageImports(node, state, extensionPath);
267
+ _collectVariableDeclarations(node, state);
268
+ _collectDataDependencies(node, state, logger);
269
+ }
270
+ catch (e) {
271
+ // Don't let individual node processing errors crash the entire traverse.
272
+ logger.warn(`Error processing node: ${JSON.stringify(node)}: ${e instanceof Error ? e.message : String(e)}`);
273
+ }
274
+ },
275
+ });
276
+ }
277
+ catch (e) {
278
+ logger.warn(`Unable to traverse AST: ${e instanceof Error ? e.message : String(e)}`);
279
+ }
280
+ return state;
281
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};