@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.
Files changed (95) hide show
  1. package/README.md +23 -4
  2. package/dist/index.d.ts +3 -3
  3. package/dist/index.js +4 -45
  4. package/dist/lib/DevModeInterface.d.ts +2 -2
  5. package/dist/lib/DevModeInterface.js +12 -28
  6. package/dist/lib/DevModeParentInterface.d.ts +2 -2
  7. package/dist/lib/DevModeParentInterface.js +138 -154
  8. package/dist/lib/DevModeUnifiedInterface.d.ts +2 -2
  9. package/dist/lib/DevModeUnifiedInterface.js +28 -49
  10. package/dist/lib/DevServerState.d.ts +9 -5
  11. package/dist/lib/DevServerState.js +37 -18
  12. package/dist/lib/ExtensionsWebSocket.d.ts +25 -0
  13. package/dist/lib/ExtensionsWebSocket.js +110 -0
  14. package/dist/lib/__mocks__/config.d.ts +2 -0
  15. package/dist/lib/__mocks__/config.js +5 -0
  16. package/dist/lib/__mocks__/isExtensionFile.d.ts +5 -0
  17. package/dist/lib/__mocks__/isExtensionFile.js +11 -0
  18. package/dist/lib/__tests__/DevModeInterface.spec.d.ts +1 -0
  19. package/dist/lib/__tests__/DevModeInterface.spec.js +155 -0
  20. package/dist/lib/__tests__/DevModeParentInterface.spec.d.ts +1 -0
  21. package/dist/lib/__tests__/DevModeParentInterface.spec.js +179 -0
  22. package/dist/lib/__tests__/DevModeUnifiedInterface.spec.d.ts +1 -0
  23. package/dist/lib/__tests__/DevModeUnifiedInterface.spec.js +236 -0
  24. package/dist/lib/__tests__/ExtensionsWebSocket.spec.d.ts +1 -0
  25. package/dist/lib/__tests__/ExtensionsWebSocket.spec.js +304 -0
  26. package/dist/lib/__tests__/ast.spec.d.ts +1 -0
  27. package/dist/lib/__tests__/ast.spec.js +737 -0
  28. package/dist/lib/__tests__/build.spec.d.ts +1 -0
  29. package/dist/lib/__tests__/build.spec.js +159 -0
  30. package/dist/lib/__tests__/config.spec.d.ts +1 -0
  31. package/dist/lib/__tests__/config.spec.js +291 -0
  32. package/dist/lib/__tests__/dev.spec.d.ts +1 -0
  33. package/dist/lib/__tests__/dev.spec.js +80 -0
  34. package/dist/lib/__tests__/extensionsService.spec.d.ts +1 -0
  35. package/dist/lib/__tests__/extensionsService.spec.js +150 -0
  36. package/dist/lib/__tests__/factories.d.ts +48 -0
  37. package/dist/lib/__tests__/factories.js +32 -0
  38. package/dist/lib/__tests__/fixtures/extensionConfig.d.ts +182 -0
  39. package/dist/lib/__tests__/fixtures/extensionConfig.js +304 -0
  40. package/dist/lib/__tests__/fixtures/urls.d.ts +4 -0
  41. package/dist/lib/__tests__/fixtures/urls.js +4 -0
  42. package/dist/lib/__tests__/parsing-utils.spec.d.ts +1 -0
  43. package/dist/lib/__tests__/parsing-utils.spec.js +467 -0
  44. package/dist/lib/__tests__/plugins/codeBlockingPlugin.spec.d.ts +1 -0
  45. package/dist/lib/__tests__/plugins/codeBlockingPlugin.spec.js +112 -0
  46. package/dist/lib/__tests__/plugins/codeCheckingPlugin.spec.d.ts +1 -0
  47. package/dist/lib/__tests__/plugins/codeCheckingPlugin.spec.js +82 -0
  48. package/dist/lib/__tests__/plugins/devBuildPlugin.spec.d.ts +1 -0
  49. package/dist/lib/__tests__/plugins/devBuildPlugin.spec.js +256 -0
  50. package/dist/lib/__tests__/plugins/friendlyLoggingPlugin.spec.d.ts +1 -0
  51. package/dist/lib/__tests__/plugins/friendlyLoggingPlugin.spec.js +65 -0
  52. package/dist/lib/__tests__/plugins/manifestPlugin.spec.d.ts +1 -0
  53. package/dist/lib/__tests__/plugins/manifestPlugin.spec.js +455 -0
  54. package/dist/lib/__tests__/plugins/relevantModulesPlugin.spec.d.ts +1 -0
  55. package/dist/lib/__tests__/plugins/relevantModulesPlugin.spec.js +81 -0
  56. package/dist/lib/__tests__/server.spec.d.ts +1 -0
  57. package/dist/lib/__tests__/server.spec.js +152 -0
  58. package/dist/lib/__tests__/test-utils/ast.d.ts +1 -0
  59. package/dist/lib/__tests__/test-utils/ast.js +4 -0
  60. package/dist/lib/__tests__/utils.spec.d.ts +1 -0
  61. package/dist/lib/__tests__/utils.spec.js +176 -0
  62. package/dist/lib/ast.d.ts +1 -1
  63. package/dist/lib/ast.js +22 -29
  64. package/dist/lib/bin/cli.js +52 -72
  65. package/dist/lib/build.d.ts +1 -1
  66. package/dist/lib/build.js +60 -78
  67. package/dist/lib/config.d.ts +1 -1
  68. package/dist/lib/config.js +31 -34
  69. package/dist/lib/constants.d.ts +0 -2
  70. package/dist/lib/constants.js +20 -27
  71. package/dist/lib/dev.d.ts +1 -1
  72. package/dist/lib/dev.js +52 -69
  73. package/dist/lib/extensionsService.d.ts +1 -1
  74. package/dist/lib/extensionsService.js +21 -15
  75. package/dist/lib/parsing-utils.d.ts +1 -1
  76. package/dist/lib/parsing-utils.js +7 -11
  77. package/dist/lib/plugins/codeBlockingPlugin.d.ts +1 -1
  78. package/dist/lib/plugins/codeBlockingPlugin.js +5 -8
  79. package/dist/lib/plugins/codeCheckingPlugin.d.ts +1 -1
  80. package/dist/lib/plugins/codeCheckingPlugin.js +10 -11
  81. package/dist/lib/plugins/devBuildPlugin.d.ts +2 -2
  82. package/dist/lib/plugins/devBuildPlugin.js +74 -99
  83. package/dist/lib/plugins/friendlyLoggingPlugin.d.ts +2 -2
  84. package/dist/lib/plugins/friendlyLoggingPlugin.js +4 -12
  85. package/dist/lib/plugins/manifestPlugin.d.ts +1 -1
  86. package/dist/lib/plugins/manifestPlugin.js +46 -26
  87. package/dist/lib/plugins/relevantModulesPlugin.d.ts +2 -2
  88. package/dist/lib/plugins/relevantModulesPlugin.js +4 -7
  89. package/dist/lib/server.d.ts +7 -2
  90. package/dist/lib/server.js +85 -84
  91. package/dist/lib/types.d.ts +1 -1
  92. package/dist/lib/types.js +4 -7
  93. package/dist/lib/utils.d.ts +1 -1
  94. package/dist/lib/utils.js +23 -40
  95. package/package.json +44 -31
@@ -0,0 +1,467 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { traverseAbstractSyntaxTree } from "../ast.js";
3
+ import { getValueFromNode, isVariableImported, isIdentifierDefined, isFunctionInvoked, } from "../parsing-utils.js";
4
+ import { localParse } from "./test-utils/ast.js";
5
+ const emptyState = {
6
+ functions: {},
7
+ badImports: [],
8
+ variableDeclarations: new Map(),
9
+ dataDependencies: {
10
+ importedHooks: {},
11
+ dependencies: [],
12
+ },
13
+ };
14
+ const extensionPath = '/mock/extension/path';
15
+ const mockLogger = {
16
+ debug: vi.fn(),
17
+ info: vi.fn(),
18
+ warn: vi.fn(),
19
+ error: vi.fn(),
20
+ };
21
+ describe('Parsing Utils', () => {
22
+ describe('getValueFromNode', () => {
23
+ it('should extract literal values correctly', () => {
24
+ const stringProgram = localParse(`const foo = "hello world";`);
25
+ const numberProgram = localParse(`const foo = 42;`);
26
+ const booleanProgram = localParse(`const foo = true;`);
27
+ const nullProgram = localParse(`const foo = null;`);
28
+ // Extract the literal nodes from variable declarations
29
+ const stringLiteral = stringProgram.body[0].declarations[0].init;
30
+ const numberLiteral = numberProgram.body[0].declarations[0].init;
31
+ const booleanLiteral = booleanProgram.body[0].declarations[0]
32
+ .init;
33
+ const nullLiteral = nullProgram.body[0].declarations[0].init;
34
+ expect(getValueFromNode(stringLiteral, emptyState)).toEqual({
35
+ status: 'SUCCESS',
36
+ nodeValue: 'hello world',
37
+ });
38
+ expect(getValueFromNode(numberLiteral, emptyState)).toEqual({
39
+ status: 'SUCCESS',
40
+ nodeValue: 42,
41
+ });
42
+ expect(getValueFromNode(booleanLiteral, emptyState)).toEqual({
43
+ status: 'SUCCESS',
44
+ nodeValue: true,
45
+ });
46
+ expect(getValueFromNode(nullLiteral, emptyState)).toEqual({
47
+ status: 'SUCCESS',
48
+ nodeValue: null,
49
+ });
50
+ });
51
+ it('should return identifier values correctly', () => {
52
+ const undefinedProgram = localParse(`const foo = undefined;`);
53
+ const nanProgram = localParse(`const foo = NaN;`);
54
+ const infinityProgram = localParse(`const foo = Infinity;`);
55
+ const identifierProgram = localParse(`const foo = 'myVar';`);
56
+ // Extract the identifier nodes from variable declarations
57
+ const undefinedIdentifier = undefinedProgram.body[0]
58
+ .declarations[0].init;
59
+ const nanIdentifier = nanProgram.body[0].declarations[0].init;
60
+ const infinityIdentifier = infinityProgram.body[0]
61
+ .declarations[0].init;
62
+ const identifierNode = identifierProgram.body[0].declarations[0]
63
+ .init;
64
+ const identifierOutput = traverseAbstractSyntaxTree(identifierProgram, [], extensionPath, mockLogger);
65
+ expect(getValueFromNode(undefinedIdentifier, emptyState)).toEqual({
66
+ status: 'SUCCESS',
67
+ nodeValue: undefined,
68
+ });
69
+ expect(getValueFromNode(nanIdentifier, emptyState)).toEqual({
70
+ status: 'SUCCESS',
71
+ nodeValue: NaN,
72
+ });
73
+ expect(getValueFromNode(infinityIdentifier, emptyState)).toEqual({
74
+ status: 'SUCCESS',
75
+ nodeValue: Infinity,
76
+ });
77
+ expect(getValueFromNode(identifierNode, identifierOutput)).toEqual({
78
+ status: 'SUCCESS',
79
+ nodeValue: 'myVar',
80
+ });
81
+ });
82
+ it('should extract array expressions correctly', () => {
83
+ const program = localParse(`const foo = [1, "hello", true, null];`);
84
+ const arrayExpression = program.body[0].declarations[0].init;
85
+ expect(getValueFromNode(arrayExpression, emptyState)).toEqual({
86
+ status: 'SUCCESS',
87
+ nodeValue: [1, 'hello', true, null],
88
+ });
89
+ });
90
+ it('should handle nested arrays correctly', () => {
91
+ const program = localParse(`const foo = [1, [2, 3], ["nested"]];`);
92
+ const arrayExpression = program.body[0].declarations[0].init;
93
+ expect(getValueFromNode(arrayExpression, emptyState)).toEqual({
94
+ status: 'SUCCESS',
95
+ nodeValue: [1, [2, 3], ['nested']],
96
+ });
97
+ });
98
+ it('should handle arrays with null/undefined elements', () => {
99
+ const program = localParse(`const foo = [1, null, 3, undefined];`);
100
+ const arrayExpression = program.body[0].declarations[0].init;
101
+ expect(getValueFromNode(arrayExpression, emptyState)).toEqual({
102
+ status: 'SUCCESS',
103
+ nodeValue: [1, null, 3, undefined],
104
+ });
105
+ });
106
+ it('should handle arrays with spread operators', () => {
107
+ const program = localParse(`const foo = [1, 2, 3]; const bar = [...foo, ...[4, 5], 6];`);
108
+ const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
109
+ const arrayExpression = program.body[1].declarations[0].init;
110
+ expect(getValueFromNode(arrayExpression, output)).toEqual({
111
+ status: 'SUCCESS',
112
+ nodeValue: [1, 2, 3, 4, 5, 6],
113
+ });
114
+ });
115
+ it('should fail gracefully for unsupported spread operators', () => {
116
+ const program = localParse(`const foo = () => [1, 2, 3]; const bar = [...foo(), ...[4, 5], 6];`);
117
+ const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
118
+ const arrayExpression = program.body[1].declarations[0].init;
119
+ const result = getValueFromNode(arrayExpression, output);
120
+ expect(result.status).toBe('FAIL');
121
+ expect(result.error).toBe('Array spread element failed: Unsupported SpreadElement type: CallExpression');
122
+ });
123
+ it('should extract simple object expressions correctly', () => {
124
+ const program = localParse(`const foo = { name: "John", age: 30 };`);
125
+ const objectExpression = program.body[0].declarations[0].init;
126
+ expect(getValueFromNode(objectExpression, emptyState)).toEqual({
127
+ status: 'SUCCESS',
128
+ nodeValue: {
129
+ name: 'John',
130
+ age: 30,
131
+ },
132
+ });
133
+ });
134
+ it('should handle object expressions with identifier keys', () => {
135
+ const program = localParse(`const foo = { name: "John", age: 30 };`);
136
+ const objectExpression = program.body[0].declarations[0].init;
137
+ expect(getValueFromNode(objectExpression, emptyState)).toEqual({
138
+ status: 'SUCCESS',
139
+ nodeValue: {
140
+ name: 'John',
141
+ age: 30,
142
+ },
143
+ });
144
+ });
145
+ it('should handle object expressions with literal keys', () => {
146
+ const program = localParse(`const foo = { "full-name": "John Doe", "age": 30 };`);
147
+ const objectExpression = program.body[0].declarations[0].init;
148
+ expect(getValueFromNode(objectExpression, emptyState)).toEqual({
149
+ status: 'SUCCESS',
150
+ nodeValue: {
151
+ 'full-name': 'John Doe',
152
+ age: 30,
153
+ },
154
+ });
155
+ });
156
+ it('should handle nested object expressions correctly', () => {
157
+ const program = localParse(`const foo = { person: { name: "John", details: { age: 30 } } };`);
158
+ const objectExpression = program.body[0].declarations[0].init;
159
+ expect(getValueFromNode(objectExpression, emptyState)).toEqual({
160
+ status: 'SUCCESS',
161
+ nodeValue: {
162
+ person: {
163
+ name: 'John',
164
+ details: {
165
+ age: 30,
166
+ },
167
+ },
168
+ },
169
+ });
170
+ });
171
+ it('should handle complex nested structures', () => {
172
+ const program = localParse(`const foo = { users: ["John", "Jane"], config: { enabled: true, count: 5 } };`);
173
+ const objectExpression = program.body[0].declarations[0].init;
174
+ expect(getValueFromNode(objectExpression, emptyState)).toEqual({
175
+ status: 'SUCCESS',
176
+ nodeValue: {
177
+ users: ['John', 'Jane'],
178
+ config: {
179
+ enabled: true,
180
+ count: 5,
181
+ },
182
+ },
183
+ });
184
+ });
185
+ it('should handle objects with mixed key types', () => {
186
+ const program = localParse(`const foo = { name: "John", "age": 30, 42: "answer" };`);
187
+ const objectExpression = program.body[0].declarations[0].init;
188
+ expect(getValueFromNode(objectExpression, emptyState)).toEqual({
189
+ status: 'SUCCESS',
190
+ nodeValue: {
191
+ name: 'John',
192
+ age: 30,
193
+ '42': 'answer',
194
+ },
195
+ });
196
+ });
197
+ it('should handle objects with spread operators', () => {
198
+ const program = localParse(`const subObject = { akey: 'astring', bkey: 'bstring' }; const bar = {...subObject, ckey: 'cstring', akey: 'anotherstring'}`);
199
+ const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
200
+ const objectExpression = program.body[1].declarations[0].init;
201
+ expect(getValueFromNode(objectExpression, output)).toEqual({
202
+ status: 'SUCCESS',
203
+ nodeValue: {
204
+ akey: 'anotherstring',
205
+ bkey: 'bstring',
206
+ ckey: 'cstring',
207
+ },
208
+ });
209
+ });
210
+ it('should return error message for unsupported node types', () => {
211
+ const program = localParse(`const foo = () => {};`);
212
+ const functionExpression = program.body[0].declarations[0].init;
213
+ const result = getValueFromNode(functionExpression, emptyState);
214
+ expect(result).toEqual({
215
+ status: 'FAIL',
216
+ error: 'Unsupported node type: ArrowFunctionExpression',
217
+ });
218
+ });
219
+ it('should handle empty arrays correctly', () => {
220
+ const program = localParse(`const foo = [];`);
221
+ const arrayExpression = program.body[0].declarations[0].init;
222
+ expect(getValueFromNode(arrayExpression, emptyState)).toEqual({
223
+ status: 'SUCCESS',
224
+ nodeValue: [],
225
+ });
226
+ });
227
+ it('should handle empty objects correctly', () => {
228
+ const program = localParse(`const foo = {};`);
229
+ const objectExpression = program.body[0].declarations[0].init;
230
+ expect(getValueFromNode(objectExpression, emptyState)).toEqual({
231
+ status: 'SUCCESS',
232
+ nodeValue: {},
233
+ });
234
+ });
235
+ it('should skip object properties with unsupported key types', () => {
236
+ // Create a mock node to simulate an unsupported key type
237
+ const mockObjectExpression = {
238
+ type: 'ObjectExpression',
239
+ properties: [
240
+ {
241
+ type: 'Property',
242
+ key: { type: 'MemberExpression' }, // Unsupported key type
243
+ value: { type: 'Literal', value: 'test' },
244
+ kind: 'init',
245
+ method: false,
246
+ shorthand: false,
247
+ computed: false,
248
+ },
249
+ ],
250
+ };
251
+ expect(getValueFromNode(mockObjectExpression, emptyState)).toEqual({
252
+ status: 'SUCCESS',
253
+ nodeValue: {},
254
+ });
255
+ });
256
+ it('should handle template literals with various interpolations', () => {
257
+ const testCases = [
258
+ {
259
+ code: `const name = 'John'; const result = \`Hello \${name}\`;`,
260
+ expected: 'Hello John',
261
+ },
262
+ {
263
+ code: `const first = 'John'; const last = 'Doe'; const result = \`\${first} \${last}\`;`,
264
+ expected: 'John Doe',
265
+ },
266
+ { code: `const result = \`Hello World\`;`, expected: 'Hello World' },
267
+ {
268
+ code: `const name = 'John'; const result = \`\${name}\`;`,
269
+ expected: 'John',
270
+ },
271
+ { code: `const result = \`\`;`, expected: '' },
272
+ {
273
+ code: `const age = 25; const result = \`Age: \${age}\`;`,
274
+ expected: 'Age: 25',
275
+ },
276
+ {
277
+ code: `const flag = true; const result = \`Status: \${flag}\`;`,
278
+ expected: 'Status: true',
279
+ },
280
+ {
281
+ code: `const val = null; const result = \`Value: \${val}\`;`,
282
+ expected: 'Value: null',
283
+ },
284
+ {
285
+ code: `const arr = ['a', 'b']; const result = \`List: \${arr}\`;`,
286
+ expected: 'List: a,b',
287
+ },
288
+ {
289
+ code: `const obj = {name: 'test'}; const result = \`Obj: \${obj}\`;`,
290
+ expected: 'Obj: [object Object]',
291
+ },
292
+ ];
293
+ testCases.forEach(({ code, expected }) => {
294
+ const program = localParse(code);
295
+ const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
296
+ const templateLiteral = program.body[program.body.length - 1]
297
+ .declarations[0].init;
298
+ expect(getValueFromNode(templateLiteral, output)).toEqual({
299
+ status: 'SUCCESS',
300
+ nodeValue: expected,
301
+ });
302
+ });
303
+ });
304
+ it('should handle unary expressions correctly', () => {
305
+ const testCases = [
306
+ { code: `const num = 5; const result = -num;`, expected: -5 },
307
+ { code: `const num = 5; const result = +num;`, expected: 5 },
308
+ { code: `const flag = true; const result = !flag;`, expected: false },
309
+ {
310
+ code: `const val = 'test'; const result = typeof val;`,
311
+ expected: 'string',
312
+ },
313
+ ];
314
+ testCases.forEach(({ code, expected }) => {
315
+ const program = localParse(code);
316
+ const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
317
+ const unaryExpression = program.body[program.body.length - 1]
318
+ .declarations[0].init;
319
+ expect(getValueFromNode(unaryExpression, output)).toEqual({
320
+ status: 'SUCCESS',
321
+ nodeValue: expected,
322
+ });
323
+ });
324
+ });
325
+ it('should fail for unsupported unary operators', () => {
326
+ const program = localParse(`const result = void 0;`);
327
+ const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
328
+ const unaryExpression = program.body[0].declarations[0].init;
329
+ expect(getValueFromNode(unaryExpression, output)).toEqual({
330
+ status: 'FAIL',
331
+ error: 'Unsupported unary operator: void',
332
+ });
333
+ });
334
+ it('should handle member expressions with variable resolution', () => {
335
+ const program = localParse(`
336
+ const config = { prefix: 'user', nested: { suffix: 'name' } };
337
+ const simple = config.prefix;
338
+ const nested = config.nested.suffix;
339
+ const deep = config.nested;
340
+ `);
341
+ const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
342
+ const simpleMember = program.body[1].declarations[0].init;
343
+ const nestedMember = program.body[2].declarations[0].init;
344
+ const deepMember = program.body[3].declarations[0].init;
345
+ expect(getValueFromNode(simpleMember, output)).toEqual({
346
+ status: 'SUCCESS',
347
+ nodeValue: 'user',
348
+ });
349
+ expect(getValueFromNode(nestedMember, output)).toEqual({
350
+ status: 'SUCCESS',
351
+ nodeValue: 'name',
352
+ });
353
+ expect(getValueFromNode(deepMember, output)).toEqual({
354
+ status: 'SUCCESS',
355
+ nodeValue: {
356
+ suffix: 'name',
357
+ },
358
+ });
359
+ });
360
+ it('should resolve identifiers to declared variables or return unsupported message as fallback', () => {
361
+ const program = localParse(`
362
+ const prefix = 'user';
363
+ const config = { type: 'contact' };
364
+ const resolvedVar = prefix;
365
+ const resolvedObj = config;
366
+ const undeclaredVar = someUndefinedVariable;
367
+ `);
368
+ const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
369
+ const resolved1 = program.body[2].declarations[0].init;
370
+ const resolved2 = program.body[3].declarations[0].init;
371
+ const undeclared = program.body[4].declarations[0].init;
372
+ expect(getValueFromNode(resolved1, output)).toEqual({
373
+ status: 'SUCCESS',
374
+ nodeValue: 'user',
375
+ });
376
+ expect(getValueFromNode(resolved2, output)).toEqual({
377
+ status: 'SUCCESS',
378
+ nodeValue: {
379
+ type: 'contact',
380
+ },
381
+ });
382
+ expect(getValueFromNode(undeclared, output)).toEqual({
383
+ status: 'FAIL',
384
+ error: 'Identifier someUndefinedVariable is not found',
385
+ });
386
+ });
387
+ });
388
+ describe('isVariableImported', () => {
389
+ it('should return true for ImportSpecifier with matching variable name', () => {
390
+ const program = localParse(`import { React } from 'react';`);
391
+ const importSpecifier = program.body[0].specifiers[0];
392
+ expect(isVariableImported(importSpecifier, 'React')).toBe(true);
393
+ expect(isVariableImported(importSpecifier, 'Component')).toBe(false);
394
+ });
395
+ it('should return true for ImportDefaultSpecifier with matching variable name', () => {
396
+ const program = localParse(`import React from 'react';`);
397
+ const importDefaultSpecifier = program.body[0].specifiers[0];
398
+ expect(isVariableImported(importDefaultSpecifier, 'React')).toBe(true);
399
+ expect(isVariableImported(importDefaultSpecifier, 'Component')).toBe(false);
400
+ });
401
+ it('should return true for ImportNamespaceSpecifier with matching variable name', () => {
402
+ const program = localParse(`import * as React from 'react';`);
403
+ const importNamespaceSpecifier = program.body[0].specifiers[0];
404
+ expect(isVariableImported(importNamespaceSpecifier, 'React')).toBe(true);
405
+ expect(isVariableImported(importNamespaceSpecifier, 'Component')).toBe(false);
406
+ });
407
+ it('should return false for null or undefined node', () => {
408
+ expect(isVariableImported(null, 'React')).toBe(false);
409
+ expect(isVariableImported(undefined, 'React')).toBe(false);
410
+ });
411
+ it('should return false for non-import node types', () => {
412
+ const program = localParse(`const foo = 'bar';`);
413
+ const variableDeclarator = program.body[0].declarations[0];
414
+ expect(isVariableImported(variableDeclarator, 'foo')).toBe(false);
415
+ });
416
+ });
417
+ describe('isIdentifierDefined', () => {
418
+ it('should return true for Identifier node with matching name and no restricting parent', () => {
419
+ const program = localParse(`const foo = bar;`);
420
+ const identifier = program.body[0].declarations[0].init;
421
+ expect(isIdentifierDefined(identifier, null, 'bar')).toBe(true);
422
+ expect(isIdentifierDefined(identifier, null, 'baz')).toBe(false);
423
+ });
424
+ it('should return false when parent is MemberExpression', () => {
425
+ const program = localParse(`const foo = obj.bar;`);
426
+ const memberExpression = program.body[0].declarations[0].init;
427
+ const identifier = memberExpression.property;
428
+ expect(isIdentifierDefined(identifier, memberExpression, 'bar')).toBe(false);
429
+ });
430
+ it('should return false when parent is CallExpression', () => {
431
+ const program = localParse(`const foo = func();`);
432
+ const callExpression = program.body[0].declarations[0].init;
433
+ const identifier = callExpression.callee;
434
+ expect(isIdentifierDefined(identifier, callExpression, 'func')).toBe(false);
435
+ });
436
+ it('should return false for non-Identifier node types', () => {
437
+ const program = localParse(`const foo = 'bar';`);
438
+ const literal = program.body[0].declarations[0].init;
439
+ expect(isIdentifierDefined(literal, null, 'bar')).toBe(false);
440
+ });
441
+ });
442
+ describe('isFunctionInvoked', () => {
443
+ it('should return true for CallExpression with matching function name', () => {
444
+ const program = localParse(`const result = myFunction();`);
445
+ const callExpression = program.body[0].declarations[0].init;
446
+ expect(isFunctionInvoked(callExpression, 'myFunction')).toBe(true);
447
+ expect(isFunctionInvoked(callExpression, 'otherFunction')).toBe(false);
448
+ });
449
+ it('should return false for CallExpression with member expression callee', () => {
450
+ const program = localParse(`const result = obj.method();`);
451
+ const callExpression = program.body[0].declarations[0].init;
452
+ expect(isFunctionInvoked(callExpression, 'method')).toBe(false);
453
+ });
454
+ it('should return false for non-CallExpression node types', () => {
455
+ const program = localParse(`const foo = 'bar';`);
456
+ const literal = program.body[0].declarations[0].init;
457
+ expect(isFunctionInvoked(literal, 'bar')).toBe(false);
458
+ });
459
+ it('should return false for CallExpression without callee name', () => {
460
+ const mockCallExpression = {
461
+ type: 'CallExpression',
462
+ callee: { type: 'Literal', value: 'test' },
463
+ };
464
+ expect(isFunctionInvoked(mockCallExpression, 'test')).toBe(false);
465
+ });
466
+ });
467
+ });
@@ -0,0 +1,112 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ vi.mock('../../ast', () => ({
3
+ traverseAbstractSyntaxTree: vi.fn((input) => {
4
+ if (input === 'invalid') {
5
+ return {
6
+ functions: {
7
+ require: {
8
+ invoked: true,
9
+ scope: 'Global',
10
+ },
11
+ },
12
+ };
13
+ }
14
+ if (input === 'outOfBounds') {
15
+ return {
16
+ functions: {},
17
+ badImports: ['/outside/path/file'],
18
+ };
19
+ }
20
+ return {
21
+ functions: {
22
+ require: {
23
+ invoked: true,
24
+ defined: true,
25
+ scope: 'Local',
26
+ },
27
+ },
28
+ };
29
+ }),
30
+ }));
31
+ import codeBlockingPlugin from "../../plugins/codeBlockingPlugin.js";
32
+ import { traverseAbstractSyntaxTree } from "../../ast.js";
33
+ describe('codeBlockingPlugin', () => {
34
+ let plugin;
35
+ let logger;
36
+ const extensionPath = '/mock/extension/path';
37
+ beforeEach(() => {
38
+ logger = {
39
+ info: vi.fn(),
40
+ error: vi.fn(),
41
+ debug: vi.fn(),
42
+ warn: vi.fn(),
43
+ };
44
+ plugin = codeBlockingPlugin({ logger, extensionPath });
45
+ });
46
+ afterEach(() => {
47
+ vi.clearAllMocks();
48
+ });
49
+ describe('metadata', () => {
50
+ it('should create the correct plugin metadata', () => {
51
+ expect(plugin).toStrictEqual(expect.objectContaining({
52
+ name: 'ui-extensions-code-blocking-plugin',
53
+ transform: expect.any(Function),
54
+ }));
55
+ });
56
+ });
57
+ describe('transform', () => {
58
+ it('should log a warning when require is being invoked', () => {
59
+ const code = 'invalid';
60
+ // @ts-expect-error It does exist, I promise
61
+ plugin.parse = vi.fn((input) => {
62
+ return input;
63
+ });
64
+ // @ts-expect-error Deep breath TS, it's ok
65
+ plugin.transform(code, 'filename');
66
+ expect(logger.warn).toHaveBeenCalledWith('require statements are not supported, replace require statements with import');
67
+ // @ts-expect-error It does exist, I promise
68
+ expect(plugin.parse).toBeCalledTimes(1);
69
+ // @ts-expect-error It does exist, I promise
70
+ expect(plugin.parse).toBeCalledWith(code);
71
+ expect(traverseAbstractSyntaxTree).toHaveBeenCalledTimes(1);
72
+ });
73
+ it('should log a warning when an out of bounds import is detected', () => {
74
+ const code = 'outOfBounds';
75
+ // @ts-expect-error It does exist, I promise
76
+ plugin.parse = vi.fn((input) => {
77
+ return input;
78
+ });
79
+ // @ts-expect-error Deep breath TS, it's ok
80
+ plugin.transform(code, 'filename');
81
+ expect(logger.warn).toHaveBeenCalledWith('Importing files from outside of the extension directory is not supported. Please move the import /outside/path/file into the extension directory.');
82
+ // @ts-expect-error It does exist, I promise
83
+ expect(plugin.parse).toBeCalledTimes(1);
84
+ expect(traverseAbstractSyntaxTree).toHaveBeenCalledTimes(1);
85
+ });
86
+ it('should not parse node_modules', () => {
87
+ // @ts-expect-error It does exist, I promise
88
+ plugin.parse = vi.fn(() => {
89
+ return {};
90
+ });
91
+ // @ts-expect-error Deep breath TS, it's ok
92
+ plugin.transform('valid', 'node_modules/filename');
93
+ // @ts-expect-error It does exist, I promise
94
+ expect(plugin.parse).not.toHaveBeenCalled();
95
+ });
96
+ it('should not throw an error for valid code', () => {
97
+ const code = 'valid';
98
+ // @ts-expect-error It does exist, I promise
99
+ plugin.parse = vi.fn(() => {
100
+ return {};
101
+ });
102
+ // @ts-expect-error Deep breath TS, it's ok
103
+ const result = plugin.transform(code, 'filename');
104
+ // @ts-expect-error It does exist, I promise
105
+ expect(plugin.parse).toBeCalledTimes(1);
106
+ // @ts-expect-error It does exist, I promise
107
+ expect(plugin.parse).toBeCalledWith(code);
108
+ expect(traverseAbstractSyntaxTree).toHaveBeenCalledTimes(1);
109
+ expect(result.code).toBe(code);
110
+ });
111
+ });
112
+ });
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ vi.mock('fs');
3
+ import codeCheckingPlugin from "../../plugins/codeCheckingPlugin.js";
4
+ import fs from 'fs';
5
+ describe('codeCheckingPlugin', () => {
6
+ let options;
7
+ let plugin;
8
+ let logger;
9
+ beforeEach(() => {
10
+ logger = {
11
+ info: vi.fn(),
12
+ error: vi.fn(),
13
+ debug: vi.fn(),
14
+ warn: vi.fn(),
15
+ };
16
+ options = {
17
+ output: 'test.jsx',
18
+ logger,
19
+ };
20
+ plugin = codeCheckingPlugin(options);
21
+ });
22
+ afterEach(() => {
23
+ vi.clearAllMocks();
24
+ });
25
+ describe('metadata', () => {
26
+ it('should create the correct plugin metadata', () => {
27
+ expect(plugin).toStrictEqual(expect.objectContaining({
28
+ name: 'ui-extensions-code-checking-plugin',
29
+ enforce: 'post',
30
+ writeBundle: expect.any(Function),
31
+ }));
32
+ });
33
+ });
34
+ describe('writeBundle', () => {
35
+ it('should log a warning if the expected code is not found', () => {
36
+ // @ts-expect-error Mock
37
+ fs.readFileSync = vi.fn(() => {
38
+ return `not the code we want, not hubspot extend call in here`;
39
+ });
40
+ // @ts-expect-error TS thinks these aren't functions
41
+ plugin.writeBundle('', '');
42
+ expect(logger.warn).toHaveBeenCalledTimes(1);
43
+ expect(logger.warn).toHaveBeenCalledWith('Unable to determine if your extension entry point is calling hubspot.extend, this may prevent it from rendering as expected');
44
+ });
45
+ it('should not log a warning if the old extend method is found', () => {
46
+ // @ts-expect-error Mock
47
+ fs.readFileSync = vi.fn(() => {
48
+ return `someFunction();const extend = (...args) => self.extend(...args);someOtherFunc()`;
49
+ });
50
+ // @ts-expect-error TS thinks these aren't functions
51
+ plugin.writeBundle('', '');
52
+ expect(logger.warn).not.toHaveBeenCalled();
53
+ });
54
+ it('should not log a warning if the new extend method is found', () => {
55
+ // @ts-expect-error Mock
56
+ fs.readFileSync = vi.fn(() => {
57
+ return `someFunction();self.extend_V2(renderExtensionCallback);someOtherFunc()`;
58
+ });
59
+ // @ts-expect-error TS thinks these aren't functions
60
+ plugin.writeBundle('', '');
61
+ expect(logger.warn).not.toHaveBeenCalled();
62
+ });
63
+ it('should not log a warning if the v0.11.3+ extend method implementation is found', () => {
64
+ // @ts-expect-error Mock
65
+ fs.readFileSync = vi.fn(() => {
66
+ return `someFunction();const extend_V2 = getWorkerGlobals().extend_V2;someOtherFunc()`;
67
+ });
68
+ // @ts-expect-error TS thinks these aren't functions
69
+ plugin.writeBundle('', '');
70
+ expect(logger.warn).not.toHaveBeenCalled();
71
+ });
72
+ it('should log an error if we are unable to load the code', () => {
73
+ fs.readFileSync = vi.fn(() => {
74
+ throw new Error('OH NO');
75
+ });
76
+ // @ts-expect-error TS thinks these aren't functions
77
+ plugin.writeBundle('', '');
78
+ expect(logger.error).toHaveBeenCalledTimes(1);
79
+ expect(logger.error).toHaveBeenCalledWith('Unable to load bundle for code checking');
80
+ });
81
+ });
82
+ });