@hubspot/eslint-config-ui-extensions 1.0.0 → 1.1.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 CHANGED
@@ -180,6 +180,7 @@ export default defineConfig([
180
180
  | [no-browser-storage](docs/rules/no-browser-storage.md) | Prevent usage of browser storage APIs (localStorage and sessionStorage) in UI Extensions, which run in a sandboxed web worker environment where these APIs are not available. | |
181
181
  | [no-console](docs/rules/no-console.md) | Prevent usage of console methods in UI Extensions in favor of the SDK logger utility. | |
182
182
  | [no-dom-access](docs/rules/no-dom-access.md) | Prevent access to the DOM (document object) in UI Extensions, which run in a sandboxed web worker without DOM access. | |
183
+ | [no-experimental-imports](docs/rules/no-experimental-imports.md) | Warn when importing from the experimental path of @hubspot/ui-extensions. | |
183
184
  | [no-html-elements](docs/rules/no-html-elements.md) | Disallow usage of unsupported HTML elements in UI Extensions. | |
184
185
  | [no-invalid-extension-point-imports](docs/rules/no-invalid-extension-point-imports.md) | Prevent importing components from unsupported extension points. | |
185
186
  | [no-invalid-image-src](docs/rules/no-invalid-image-src.md) | Only allow valid image URLs in the src attribute of Image components from @hubspot/ui-extensions. | 🔧 |
@@ -151,6 +151,26 @@ describe('config', () => {
151
151
  expect(customRuleError).toBeUndefined();
152
152
  });
153
153
  });
154
+ describe('custom no-experimental-imports rule', () => {
155
+ it('should warn when importing from experimental path', async () => {
156
+ const code = `
157
+ import { Grid } from '@hubspot/ui-extensions/experimental';
158
+ `;
159
+ const messages = await lintCode(code, 'src/app/cards/Component.tsx', '/path/to/project');
160
+ const customRuleWarning = messages.find((m) => m.ruleId === '@hubspot/ui-extensions/no-experimental-imports');
161
+ expect(customRuleWarning).toBeDefined();
162
+ expect(customRuleWarning?.severity).toBe(1);
163
+ expect(customRuleWarning?.message).toContain('experimental');
164
+ });
165
+ it('should allow stable @hubspot/ui-extensions imports', async () => {
166
+ const code = `
167
+ import { Text, Button } from '@hubspot/ui-extensions';
168
+ `;
169
+ const messages = await lintCode(code, 'src/app/cards/Component.tsx', '/path/to/project');
170
+ const customRuleWarning = messages.find((m) => m.ruleId === '@hubspot/ui-extensions/no-experimental-imports');
171
+ expect(customRuleWarning).toBeUndefined();
172
+ });
173
+ });
154
174
  describe('custom no-console rule', () => {
155
175
  it('should warn when using console.log()', async () => {
156
176
  const code = `
@@ -0,0 +1,67 @@
1
+ import { RuleTester } from 'eslint';
2
+ import { noExperimentalImports } from "../../rules/no-experimental-imports.js";
3
+ import { languageOptions } from "../../runtime.js";
4
+ const ruleTesterOptions = {
5
+ languageOptions: {
6
+ ...languageOptions,
7
+ parserOptions: {
8
+ ecmaFeatures: {
9
+ jsx: true,
10
+ },
11
+ },
12
+ },
13
+ };
14
+ const experimentalImport = `
15
+ import { Grid } from '@hubspot/ui-extensions/experimental';
16
+ `;
17
+ const experimentalSubpathImport = `
18
+ import { getDistanceFromTshirtSize } from '@hubspot/ui-extensions/experimental/Layout/constants';
19
+ `;
20
+ const stableImport = `
21
+ import { Text, Button } from '@hubspot/ui-extensions';
22
+ `;
23
+ const otherPackageImport = `
24
+ import { useState } from 'react';
25
+ `;
26
+ describe('no-experimental-imports', () => {
27
+ const ruleTester = new RuleTester(ruleTesterOptions);
28
+ ruleTester.run('no-experimental-imports', noExperimentalImports, {
29
+ valid: [
30
+ {
31
+ name: 'stable @hubspot/ui-extensions import',
32
+ code: stableImport,
33
+ filename: '/path/to/project/src/app/cards/Component.tsx',
34
+ },
35
+ {
36
+ name: 'other package import',
37
+ code: otherPackageImport,
38
+ filename: '/path/to/project/src/app/cards/Component.tsx',
39
+ },
40
+ {
41
+ name: 'crm extension point import',
42
+ code: `import { useCrmProperties } from '@hubspot/ui-extensions/crm/hooks';`,
43
+ filename: '/path/to/project/src/app/cards/Component.tsx',
44
+ },
45
+ ],
46
+ invalid: [
47
+ {
48
+ name: 'import from experimental path',
49
+ code: experimentalImport,
50
+ filename: '/path/to/project/src/app/cards/Component.tsx',
51
+ errors: [{ messageId: 'noExperimentalImports' }],
52
+ },
53
+ {
54
+ name: 'import from experimental subpath',
55
+ code: experimentalSubpathImport,
56
+ filename: '/path/to/project/src/app/extensions/Component.tsx',
57
+ errors: [{ messageId: 'noExperimentalImports' }],
58
+ },
59
+ {
60
+ name: 'default import from experimental',
61
+ code: `import ExperimentalComponent from '@hubspot/ui-extensions/experimental';`,
62
+ filename: '/path/to/project/src/app/settings/Component.tsx',
63
+ errors: [{ messageId: 'noExperimentalImports' }],
64
+ },
65
+ ],
66
+ });
67
+ });
@@ -68,6 +68,21 @@ describe('no-restricted-globals', () => {
68
68
  function navigate(location) {
69
69
  return location.href;
70
70
  }
71
+ `,
72
+ },
73
+ {
74
+ name: 'allows navigator as a local variable',
75
+ code: `
76
+ const navigator = { language: 'en' };
77
+ console.log(navigator.language);
78
+ `,
79
+ },
80
+ {
81
+ name: 'allows navigator as a function parameter',
82
+ code: `
83
+ function getLocale(navigator) {
84
+ return navigator.language;
85
+ }
71
86
  `,
72
87
  },
73
88
  {
@@ -75,6 +90,13 @@ describe('no-restricted-globals', () => {
75
90
  code: `
76
91
  const { history } = router;
77
92
  history.push('/test');
93
+ `,
94
+ },
95
+ {
96
+ name: 'allows navigator as an object property key',
97
+ code: `
98
+ const obj = { navigator: { language: 'en' } };
99
+ obj.navigator.language;
78
100
  `,
79
101
  },
80
102
  {
@@ -179,6 +201,20 @@ describe('no-restricted-globals', () => {
179
201
  `,
180
202
  errors: [{ messageId: 'history' }],
181
203
  },
204
+ {
205
+ name: 'reports error for navigator.language access',
206
+ code: `
207
+ const lang = navigator.language;
208
+ `,
209
+ errors: [{ messageId: 'navigator' }],
210
+ },
211
+ {
212
+ name: 'reports error for navigator.clipboard access',
213
+ code: `
214
+ navigator.clipboard.writeText('hello');
215
+ `,
216
+ errors: [{ messageId: 'navigator' }],
217
+ },
182
218
  {
183
219
  name: 'reports error for window.importScripts',
184
220
  code: `
@@ -207,6 +243,13 @@ describe('no-restricted-globals', () => {
207
243
  `,
208
244
  errors: [{ messageId: 'location' }],
209
245
  },
246
+ {
247
+ name: 'reports error for window.navigator',
248
+ code: `
249
+ const ua = window.navigator.userAgent;
250
+ `,
251
+ errors: [{ messageId: 'navigator' }],
252
+ },
210
253
  {
211
254
  name: 'reports error for window.history',
212
255
  code: `
@@ -228,6 +271,13 @@ describe('no-restricted-globals', () => {
228
271
  `,
229
272
  errors: [{ messageId: 'addEventListener' }],
230
273
  },
274
+ {
275
+ name: 'reports error for self.navigator',
276
+ code: `
277
+ const online = self.navigator.onLine;
278
+ `,
279
+ errors: [{ messageId: 'navigator' }],
280
+ },
231
281
  {
232
282
  name: 'reports error for self.location',
233
283
  code: `
@@ -256,6 +306,13 @@ describe('no-restricted-globals', () => {
256
306
  `,
257
307
  errors: [{ messageId: 'location' }],
258
308
  },
309
+ {
310
+ name: 'reports error for globalThis.navigator',
311
+ code: `
312
+ globalThis.navigator.geolocation.getCurrentPosition(cb);
313
+ `,
314
+ errors: [{ messageId: 'navigator' }],
315
+ },
259
316
  {
260
317
  name: 'reports error for globalThis.history',
261
318
  code: `
@@ -263,6 +320,13 @@ describe('no-restricted-globals', () => {
263
320
  `,
264
321
  errors: [{ messageId: 'history' }],
265
322
  },
323
+ {
324
+ name: 'reports error for bracket notation window["navigator"]',
325
+ code: `
326
+ const devices = window['navigator'].mediaDevices;
327
+ `,
328
+ errors: [{ messageId: 'navigator' }],
329
+ },
266
330
  {
267
331
  name: 'reports error for bracket notation window["location"]',
268
332
  code: `
@@ -284,6 +348,24 @@ describe('no-restricted-globals', () => {
284
348
  `,
285
349
  errors: [{ messageId: 'location' }],
286
350
  },
351
+ {
352
+ name: 'reports error for navigator in React component',
353
+ code: `
354
+ import { Text } from '@hubspot/ui-extensions';
355
+ function Component() {
356
+ const ua = navigator.userAgent;
357
+ return <Text>{ua}</Text>;
358
+ }
359
+ `,
360
+ errors: [{ messageId: 'navigator' }],
361
+ },
362
+ {
363
+ name: 'reports error for navigator in arrow function',
364
+ code: `
365
+ const getLangs = () => navigator.languages;
366
+ `,
367
+ errors: [{ messageId: 'navigator' }],
368
+ },
287
369
  {
288
370
  name: 'reports error in React component',
289
371
  code: `
@@ -382,6 +464,12 @@ describe('no-restricted-globals', () => {
382
464
  interface Router {
383
465
  history: typeof history;
384
466
  }
467
+ `,
468
+ },
469
+ {
470
+ name: 'allows navigator type in type alias',
471
+ code: `
472
+ type NavigatorType = typeof navigator;
385
473
  `,
386
474
  },
387
475
  {
@@ -421,6 +509,17 @@ describe('no-restricted-globals', () => {
421
509
  `,
422
510
  errors: [{ messageId: 'history' }],
423
511
  },
512
+ {
513
+ name: 'reports error for navigator usage after type declarations',
514
+ code: `
515
+ type NavigatorType = typeof navigator;
516
+
517
+ function getLocale() {
518
+ return navigator.language;
519
+ }
520
+ `,
521
+ errors: [{ messageId: 'navigator' }],
522
+ },
424
523
  {
425
524
  name: 'reports error for addEventListener with typed handler',
426
525
  code: `
@@ -1,4 +1,4 @@
1
- import { getImportedComponentName, isHubspotExtensibilityImport, isRelativeImport, } from "../../utils/import-utils.js";
1
+ import { getImportedComponentName, isExperimentalImport, isHubspotExtensibilityImport, isRelativeImport, } from "../../utils/import-utils.js";
2
2
  describe('import-utils', () => {
3
3
  describe('isHubspotExtensibilityImport', () => {
4
4
  it('should return true for @hubspot/ui-extensions imports', () => {
@@ -151,4 +151,26 @@ describe('import-utils', () => {
151
151
  expect(isRelativeImport('')).toBe(false);
152
152
  });
153
153
  });
154
+ describe('isExperimentalImport', () => {
155
+ it('should return true for @hubspot/ui-extensions/experimental', () => {
156
+ expect(isExperimentalImport('@hubspot/ui-extensions/experimental')).toBe(true);
157
+ });
158
+ it('should return true for experimental subpaths', () => {
159
+ expect(isExperimentalImport('@hubspot/ui-extensions/experimental/Layout')).toBe(true);
160
+ expect(isExperimentalImport('@hubspot/ui-extensions/experimental/Layout/Grid')).toBe(true);
161
+ });
162
+ it('should return false for stable @hubspot/ui-extensions imports', () => {
163
+ expect(isExperimentalImport('@hubspot/ui-extensions')).toBe(false);
164
+ expect(isExperimentalImport('@hubspot/ui-extensions/crm')).toBe(false);
165
+ expect(isExperimentalImport('@hubspot/ui-extensions/hooks')).toBe(false);
166
+ });
167
+ it('should return false for non-hubspot packages', () => {
168
+ expect(isExperimentalImport('react')).toBe(false);
169
+ expect(isExperimentalImport('some-package/experimental')).toBe(false);
170
+ });
171
+ it('should return false for non-string values', () => {
172
+ expect(isExperimentalImport(null)).toBe(false);
173
+ expect(isExperimentalImport(undefined)).toBe(false);
174
+ });
175
+ });
154
176
  });
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { noBrowserDialogs } from "./rules/no-browser-dialogs.js";
3
3
  import { noBrowserStorage } from "./rules/no-browser-storage.js";
4
4
  import { noConsole } from "./rules/no-console.js";
5
5
  import { noDomAccess } from "./rules/no-dom-access.js";
6
+ import { noExperimentalImports } from "./rules/no-experimental-imports.js";
6
7
  import { unsupportedHtmlElements } from "./rules/no-html-elements.js";
7
8
  import { noInvalidExtensionPointImports } from "./rules/no-invalid-extension-point-imports.js";
8
9
  import { noInvalidImageSrc } from "./rules/no-invalid-image-src.js";
@@ -18,6 +19,7 @@ const rules = {
18
19
  'no-browser-dialogs': noBrowserDialogs,
19
20
  'no-console': noConsole,
20
21
  'no-dom-access': noDomAccess,
22
+ 'no-experimental-imports': noExperimentalImports,
21
23
  'no-browser-storage': noBrowserStorage,
22
24
  'no-parent-imports': noParentImports,
23
25
  'no-restricted-globals': noRestrictedGlobals,
@@ -55,6 +57,7 @@ export const config = [
55
57
  '@hubspot/ui-extensions/no-native-http': 'error',
56
58
  '@hubspot/ui-extensions/no-browser-dialogs': 'error',
57
59
  '@hubspot/ui-extensions/no-console': 'warn',
60
+ '@hubspot/ui-extensions/no-experimental-imports': 'warn',
58
61
  '@hubspot/ui-extensions/no-dom-access': 'error',
59
62
  '@hubspot/ui-extensions/no-browser-storage': 'error',
60
63
  '@hubspot/ui-extensions/no-parent-imports': 'error',
@@ -0,0 +1,2 @@
1
+ import type { Rule } from 'eslint';
2
+ export declare const noExperimentalImports: Rule.RuleModule;
@@ -0,0 +1,29 @@
1
+ import { isExperimentalImport } from "../utils/import-utils.js";
2
+ export const noExperimentalImports = {
3
+ meta: {
4
+ type: 'suggestion',
5
+ docs: {
6
+ description: 'Warn when importing from the experimental path of @hubspot/ui-extensions.',
7
+ url: 'https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensibility/linting/rules/no-experimental-imports',
8
+ },
9
+ messages: {
10
+ noExperimentalImports: 'Imports from "{{importSource}}" use experimental APIs. Experimental components are not stable and are not recommended for production.',
11
+ },
12
+ schema: [],
13
+ },
14
+ create(context) {
15
+ return {
16
+ ImportDeclaration(node) {
17
+ const importSource = node.source.value;
18
+ if (!isExperimentalImport(importSource)) {
19
+ return;
20
+ }
21
+ context.report({
22
+ node,
23
+ messageId: 'noExperimentalImports',
24
+ data: { importSource },
25
+ });
26
+ },
27
+ };
28
+ },
29
+ };
@@ -10,6 +10,7 @@ const RESTRICTED_FUNCTION_GLOBALS = new Set([
10
10
  const RESTRICTED_VALUE_GLOBALS = new Set([
11
11
  'location',
12
12
  'history',
13
+ 'navigator',
13
14
  ]);
14
15
  // All restricted globals (for Identifier checking)
15
16
  const ALL_RESTRICTED_GLOBALS = new Set([
@@ -29,6 +30,7 @@ export const noRestrictedGlobals = {
29
30
  removeEventListener: 'removeEventListener() is not supported. Use React event props instead.',
30
31
  location: 'location is not available. Use context.location instead.',
31
32
  history: 'history is not available. Use <Link> or actions.openIframeModal() instead.',
33
+ navigator: 'navigator is not available as expected. Use provided context and actions instead.',
32
34
  },
33
35
  schema: [],
34
36
  },
package/dist/runtime.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import globals from 'globals';
2
2
  export const languageOptions = {
3
- ecmaVersion: 6,
3
+ ecmaVersion: 2020,
4
4
  sourceType: 'module',
5
5
  globals: {
6
6
  ...globals.worker,
@@ -31,3 +31,4 @@ export declare const getImportedComponentName: (node: ImportDeclaration, compone
31
31
  * @returns True if the import source is a relative import
32
32
  */
33
33
  export declare const isRelativeImport: (importSource: unknown) => importSource is string;
34
+ export declare const isExperimentalImport: (importSource: unknown) => boolean;
@@ -45,3 +45,8 @@ export const isRelativeImport = (importSource) => {
45
45
  return (typeof importSource === 'string' &&
46
46
  (importSource.startsWith('./') || importSource.startsWith('../')));
47
47
  };
48
+ export const isExperimentalImport = (importSource) => {
49
+ return (typeof importSource === 'string' &&
50
+ isHubspotExtensibilityImport(importSource) &&
51
+ importSource.includes('/experimental'));
52
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/eslint-config-ui-extensions",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "type": "module",
5
5
  "description": "eslint plugins for HubSpot React-based UI Extensions",
6
6
  "keywords": [