@hubspot/ui-extensions-dev-server 0.10.2 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -4
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -45
- package/dist/lib/DevModeInterface.d.ts +2 -2
- package/dist/lib/DevModeInterface.js +12 -28
- package/dist/lib/DevModeParentInterface.d.ts +2 -2
- package/dist/lib/DevModeParentInterface.js +138 -154
- package/dist/lib/DevModeUnifiedInterface.d.ts +2 -2
- package/dist/lib/DevModeUnifiedInterface.js +28 -49
- package/dist/lib/DevServerState.d.ts +9 -5
- package/dist/lib/DevServerState.js +37 -18
- package/dist/lib/ExtensionsWebSocket.d.ts +25 -0
- package/dist/lib/ExtensionsWebSocket.js +110 -0
- package/dist/lib/__mocks__/config.d.ts +2 -0
- package/dist/lib/__mocks__/config.js +5 -0
- package/dist/lib/__mocks__/isExtensionFile.d.ts +5 -0
- package/dist/lib/__mocks__/isExtensionFile.js +11 -0
- package/dist/lib/__tests__/DevModeInterface.spec.d.ts +1 -0
- package/dist/lib/__tests__/DevModeInterface.spec.js +155 -0
- package/dist/lib/__tests__/DevModeParentInterface.spec.d.ts +1 -0
- package/dist/lib/__tests__/DevModeParentInterface.spec.js +179 -0
- package/dist/lib/__tests__/DevModeUnifiedInterface.spec.d.ts +1 -0
- package/dist/lib/__tests__/DevModeUnifiedInterface.spec.js +236 -0
- package/dist/lib/__tests__/ExtensionsWebSocket.spec.d.ts +1 -0
- package/dist/lib/__tests__/ExtensionsWebSocket.spec.js +304 -0
- package/dist/lib/__tests__/ast.spec.d.ts +1 -0
- package/dist/lib/__tests__/ast.spec.js +737 -0
- package/dist/lib/__tests__/build.spec.d.ts +1 -0
- package/dist/lib/__tests__/build.spec.js +159 -0
- package/dist/lib/__tests__/config.spec.d.ts +1 -0
- package/dist/lib/__tests__/config.spec.js +291 -0
- package/dist/lib/__tests__/dev.spec.d.ts +1 -0
- package/dist/lib/__tests__/dev.spec.js +80 -0
- package/dist/lib/__tests__/extensionsService.spec.d.ts +1 -0
- package/dist/lib/__tests__/extensionsService.spec.js +150 -0
- package/dist/lib/__tests__/factories.d.ts +48 -0
- package/dist/lib/__tests__/factories.js +32 -0
- package/dist/lib/__tests__/fixtures/extensionConfig.d.ts +182 -0
- package/dist/lib/__tests__/fixtures/extensionConfig.js +304 -0
- package/dist/lib/__tests__/fixtures/urls.d.ts +4 -0
- package/dist/lib/__tests__/fixtures/urls.js +4 -0
- package/dist/lib/__tests__/parsing-utils.spec.d.ts +1 -0
- package/dist/lib/__tests__/parsing-utils.spec.js +467 -0
- package/dist/lib/__tests__/plugins/codeBlockingPlugin.spec.d.ts +1 -0
- package/dist/lib/__tests__/plugins/codeBlockingPlugin.spec.js +112 -0
- package/dist/lib/__tests__/plugins/codeCheckingPlugin.spec.d.ts +1 -0
- package/dist/lib/__tests__/plugins/codeCheckingPlugin.spec.js +82 -0
- package/dist/lib/__tests__/plugins/devBuildPlugin.spec.d.ts +1 -0
- package/dist/lib/__tests__/plugins/devBuildPlugin.spec.js +256 -0
- package/dist/lib/__tests__/plugins/friendlyLoggingPlugin.spec.d.ts +1 -0
- package/dist/lib/__tests__/plugins/friendlyLoggingPlugin.spec.js +65 -0
- package/dist/lib/__tests__/plugins/manifestPlugin.spec.d.ts +1 -0
- package/dist/lib/__tests__/plugins/manifestPlugin.spec.js +455 -0
- package/dist/lib/__tests__/plugins/relevantModulesPlugin.spec.d.ts +1 -0
- package/dist/lib/__tests__/plugins/relevantModulesPlugin.spec.js +81 -0
- package/dist/lib/__tests__/server.spec.d.ts +1 -0
- package/dist/lib/__tests__/server.spec.js +152 -0
- package/dist/lib/__tests__/test-utils/ast.d.ts +1 -0
- package/dist/lib/__tests__/test-utils/ast.js +4 -0
- package/dist/lib/__tests__/utils.spec.d.ts +1 -0
- package/dist/lib/__tests__/utils.spec.js +176 -0
- package/dist/lib/ast.d.ts +1 -1
- package/dist/lib/ast.js +22 -29
- package/dist/lib/bin/cli.js +52 -72
- package/dist/lib/build.d.ts +1 -1
- package/dist/lib/build.js +60 -78
- package/dist/lib/config.d.ts +1 -1
- package/dist/lib/config.js +31 -34
- package/dist/lib/constants.d.ts +0 -2
- package/dist/lib/constants.js +20 -27
- package/dist/lib/dev.d.ts +1 -1
- package/dist/lib/dev.js +52 -69
- package/dist/lib/extensionsService.d.ts +1 -1
- package/dist/lib/extensionsService.js +21 -15
- package/dist/lib/parsing-utils.d.ts +1 -1
- package/dist/lib/parsing-utils.js +7 -11
- package/dist/lib/plugins/codeBlockingPlugin.d.ts +1 -1
- package/dist/lib/plugins/codeBlockingPlugin.js +5 -8
- package/dist/lib/plugins/codeCheckingPlugin.d.ts +1 -1
- package/dist/lib/plugins/codeCheckingPlugin.js +10 -11
- package/dist/lib/plugins/devBuildPlugin.d.ts +2 -2
- package/dist/lib/plugins/devBuildPlugin.js +74 -99
- package/dist/lib/plugins/friendlyLoggingPlugin.d.ts +2 -2
- package/dist/lib/plugins/friendlyLoggingPlugin.js +4 -12
- package/dist/lib/plugins/manifestPlugin.d.ts +1 -1
- package/dist/lib/plugins/manifestPlugin.js +46 -26
- package/dist/lib/plugins/relevantModulesPlugin.d.ts +2 -2
- package/dist/lib/plugins/relevantModulesPlugin.js +4 -7
- package/dist/lib/server.d.ts +7 -2
- package/dist/lib/server.js +85 -84
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/types.js +4 -7
- package/dist/lib/utils.d.ts +1 -1
- package/dist/lib/utils.js +23 -40
- package/package.json +44 -31
|
@@ -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 @@
|
|
|
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
|
+
});
|
package/dist/lib/ast.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SourceCodeMetadata, SourceCodeChecks, Logger, NodeValue } from './types';
|
|
1
|
+
import { SourceCodeMetadata, SourceCodeChecks, Logger, NodeValue } from './types.ts';
|
|
2
2
|
import { Program, Node } from 'estree';
|
|
3
3
|
/**
|
|
4
4
|
* We only support image imports that are within the extension directory.
|
package/dist/lib/ast.js
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
/* eslint-disable hubspot-dev/no-unsupported-ts-syntax */
|
|
3
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
-
};
|
|
6
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.checkForOutOfBoundsImageImports = checkForOutOfBoundsImageImports;
|
|
8
|
-
exports.traverseAbstractSyntaxTree = traverseAbstractSyntaxTree;
|
|
9
|
-
const path_1 = __importDefault(require("path"));
|
|
1
|
+
import path from 'path';
|
|
10
2
|
// @ts-expect-error no type defs
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
import { traverse } from 'estraverse';
|
|
4
|
+
import { generateHash, isExtensionFile, isImage } from "./utils.js";
|
|
5
|
+
import { getValueFromNode, isFunctionInvoked, isIdentifierDefined, isVariableImported, } from "./parsing-utils.js";
|
|
14
6
|
const PARSED_HOOKS = ['useCrmProperties', 'useAssociations'];
|
|
15
7
|
function _checkForFunctionMetadata(node, parent, output, functionName) {
|
|
16
8
|
if (!node) {
|
|
@@ -19,7 +11,7 @@ function _checkForFunctionMetadata(node, parent, output, functionName) {
|
|
|
19
11
|
if (!output.functions[functionName]) {
|
|
20
12
|
output.functions[functionName] = {};
|
|
21
13
|
}
|
|
22
|
-
if (
|
|
14
|
+
if (isFunctionInvoked(node, functionName)) {
|
|
23
15
|
output.functions[functionName].invoked = true;
|
|
24
16
|
// If the function is invoked before being defined we will assume it is a global function
|
|
25
17
|
output.functions[functionName].scope = output.functions[functionName]
|
|
@@ -27,8 +19,8 @@ function _checkForFunctionMetadata(node, parent, output, functionName) {
|
|
|
27
19
|
? 'Local'
|
|
28
20
|
: 'Global';
|
|
29
21
|
}
|
|
30
|
-
else if (
|
|
31
|
-
|
|
22
|
+
else if (isIdentifierDefined(node, parent, functionName) ||
|
|
23
|
+
isVariableImported(node, functionName)) {
|
|
32
24
|
output.functions[functionName].defined = true;
|
|
33
25
|
}
|
|
34
26
|
}
|
|
@@ -43,7 +35,7 @@ function _collectVariableDeclarations(node, state) {
|
|
|
43
35
|
declaration.id.type === 'Identifier' &&
|
|
44
36
|
declaration.init) {
|
|
45
37
|
const variableName = declaration.id.name;
|
|
46
|
-
const result =
|
|
38
|
+
const result = getValueFromNode(declaration.init, state);
|
|
47
39
|
if (result.status === 'SUCCESS') {
|
|
48
40
|
state.variableDeclarations.set(variableName, result.nodeValue);
|
|
49
41
|
}
|
|
@@ -54,7 +46,7 @@ function _collectVariableDeclarations(node, state) {
|
|
|
54
46
|
if (node.type === 'AssignmentExpression' && node.left.type === 'Identifier') {
|
|
55
47
|
const variableName = node.left.name;
|
|
56
48
|
if (state.variableDeclarations.has(variableName)) {
|
|
57
|
-
const result =
|
|
49
|
+
const result = getValueFromNode(node.right, state);
|
|
58
50
|
if (result.status === 'SUCCESS') {
|
|
59
51
|
state.variableDeclarations.set(variableName, result.nodeValue);
|
|
60
52
|
}
|
|
@@ -65,7 +57,7 @@ function _collectVariableDeclarations(node, state) {
|
|
|
65
57
|
* We only support image imports that are within the extension directory.
|
|
66
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.
|
|
67
59
|
*/
|
|
68
|
-
function checkForOutOfBoundsImageImports(node, output, extensionPath) {
|
|
60
|
+
export function checkForOutOfBoundsImageImports(node, output, extensionPath) {
|
|
69
61
|
if (!node) {
|
|
70
62
|
return;
|
|
71
63
|
}
|
|
@@ -73,12 +65,12 @@ function checkForOutOfBoundsImageImports(node, output, extensionPath) {
|
|
|
73
65
|
typeof node.source.value === 'string') {
|
|
74
66
|
const importPath = node.source.value;
|
|
75
67
|
// Only do the check for images.
|
|
76
|
-
if (!
|
|
68
|
+
if (!isImage(importPath)) {
|
|
77
69
|
return;
|
|
78
70
|
}
|
|
79
71
|
// Build the full path to the import, using the extension path as the base.
|
|
80
|
-
const absoluteImportPath =
|
|
81
|
-
if (!
|
|
72
|
+
const absoluteImportPath = path.resolve(extensionPath, importPath);
|
|
73
|
+
if (!isExtensionFile(absoluteImportPath, extensionPath)) {
|
|
82
74
|
output.badImports.push(importPath);
|
|
83
75
|
}
|
|
84
76
|
}
|
|
@@ -91,7 +83,7 @@ function _processCrmPropertiesHook(node, output) {
|
|
|
91
83
|
const optionsNode = node.arguments[1];
|
|
92
84
|
const requestedProperties = [];
|
|
93
85
|
const propertiesResult = propertiesNode
|
|
94
|
-
?
|
|
86
|
+
? getValueFromNode(propertiesNode, output)
|
|
95
87
|
: null;
|
|
96
88
|
if (propertiesResult &&
|
|
97
89
|
propertiesResult.status === 'SUCCESS' &&
|
|
@@ -105,7 +97,7 @@ function _processCrmPropertiesHook(node, output) {
|
|
|
105
97
|
if (requestedProperties.length > 0) {
|
|
106
98
|
let options = {};
|
|
107
99
|
const optionsResult = optionsNode
|
|
108
|
-
?
|
|
100
|
+
? getValueFromNode(optionsNode, output)
|
|
109
101
|
: null;
|
|
110
102
|
if (optionsResult &&
|
|
111
103
|
optionsResult.status === 'SUCCESS' &&
|
|
@@ -116,7 +108,7 @@ function _processCrmPropertiesHook(node, output) {
|
|
|
116
108
|
options = optionsResult.nodeValue;
|
|
117
109
|
}
|
|
118
110
|
output.dataDependencies.dependencies.push({
|
|
119
|
-
referenceId:
|
|
111
|
+
referenceId: generateHash(propertyType, requestedProperties),
|
|
120
112
|
properties: {
|
|
121
113
|
type: propertyType,
|
|
122
114
|
recordProperties: requestedProperties,
|
|
@@ -132,7 +124,7 @@ function _processAssociationsHook(node, output) {
|
|
|
132
124
|
const requestNode = node.arguments[0];
|
|
133
125
|
const optionsNode = node.arguments[1];
|
|
134
126
|
const requestResult = requestNode
|
|
135
|
-
?
|
|
127
|
+
? getValueFromNode(requestNode, output)
|
|
136
128
|
: null;
|
|
137
129
|
if (!requestResult ||
|
|
138
130
|
requestResult.status === 'FAIL' ||
|
|
@@ -158,7 +150,7 @@ function _processAssociationsHook(node, output) {
|
|
|
158
150
|
const paginationOptions = {};
|
|
159
151
|
const otherOptions = {};
|
|
160
152
|
const optionsResult = optionsNode
|
|
161
|
-
?
|
|
153
|
+
? getValueFromNode(optionsNode, output)
|
|
162
154
|
: null;
|
|
163
155
|
if (optionsResult &&
|
|
164
156
|
optionsResult.status === 'SUCCESS' &&
|
|
@@ -179,7 +171,7 @@ function _processAssociationsHook(node, output) {
|
|
|
179
171
|
}
|
|
180
172
|
const sortedPropertiesForHash = [...propertiesArray].sort();
|
|
181
173
|
output.dataDependencies.dependencies.push({
|
|
182
|
-
referenceId:
|
|
174
|
+
referenceId: generateHash(propertyType, toObjectTypeId, sortedPropertiesForHash.join('-')),
|
|
183
175
|
properties: {
|
|
184
176
|
type: propertyType,
|
|
185
177
|
toObjectTypeId,
|
|
@@ -206,6 +198,7 @@ function _collectDataDependencies(node, output, logger) {
|
|
|
206
198
|
node.specifiers.forEach((specifier) => {
|
|
207
199
|
// If the specifier is an ImportSpecifier and the imported name is one of our tracked hooks, we will track it.
|
|
208
200
|
if (specifier.type === 'ImportSpecifier' &&
|
|
201
|
+
specifier.imported.type === 'Identifier' &&
|
|
209
202
|
PARSED_HOOKS.includes(specifier.imported.name)) {
|
|
210
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.
|
|
211
204
|
output.dataDependencies.importedHooks[`${specifier.local.name}`] =
|
|
@@ -253,7 +246,7 @@ function _collectDataDependencies(node, output, logger) {
|
|
|
253
246
|
}
|
|
254
247
|
// Traverses an ESTree as defined by the EsTree spec https://github.com/estree/estree
|
|
255
248
|
// Uses the checks array to search the source code for matches
|
|
256
|
-
function traverseAbstractSyntaxTree(ast, checks, extensionPath, logger) {
|
|
249
|
+
export function traverseAbstractSyntaxTree(ast, checks, extensionPath, logger) {
|
|
257
250
|
const state = {
|
|
258
251
|
functions: {},
|
|
259
252
|
badImports: [],
|
|
@@ -264,7 +257,7 @@ function traverseAbstractSyntaxTree(ast, checks, extensionPath, logger) {
|
|
|
264
257
|
variableDeclarations: new Map(),
|
|
265
258
|
};
|
|
266
259
|
try {
|
|
267
|
-
|
|
260
|
+
traverse(ast, {
|
|
268
261
|
enter(node, parent) {
|
|
269
262
|
try {
|
|
270
263
|
checks.forEach((check) => {
|