@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 +1 -0
- package/dist/__tests__/index.spec.js +20 -0
- package/dist/__tests__/rules/no-experimental-imports.spec.d.ts +1 -0
- package/dist/__tests__/rules/no-experimental-imports.spec.js +67 -0
- package/dist/__tests__/rules/no-restricted-globals.spec.js +99 -0
- package/dist/__tests__/utils/import-utils.spec.js +23 -1
- package/dist/index.js +3 -0
- package/dist/rules/no-experimental-imports.d.ts +2 -0
- package/dist/rules/no-experimental-imports.js +29 -0
- package/dist/rules/no-restricted-globals.js +2 -0
- package/dist/runtime.js +1 -1
- package/dist/utils/import-utils.d.ts +1 -0
- package/dist/utils/import-utils.js +5 -0
- package/package.json +1 -1
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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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,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
|
@@ -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
|
+
};
|