@hubspot/eslint-config-ui-extensions 1.1.0 → 1.2.0
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 +22 -1
- package/dist/__tests__/rules/no-browser-dialogs.spec.js +2 -2
- package/dist/__tests__/rules/no-browser-storage.spec.js +2 -2
- package/dist/__tests__/rules/no-console.spec.js +2 -2
- package/dist/__tests__/rules/no-dom-access.spec.js +2 -2
- package/dist/__tests__/rules/no-experimental-imports.spec.js +2 -2
- package/dist/__tests__/rules/no-html-elements.spec.js +2 -2
- package/dist/__tests__/rules/no-import-meta-env.spec.d.ts +1 -0
- package/dist/__tests__/rules/no-import-meta-env.spec.js +100 -0
- package/dist/__tests__/rules/no-invalid-extension-point-imports.spec.js +2 -2
- package/dist/__tests__/rules/no-invalid-image-src.spec.js +2 -2
- package/dist/__tests__/rules/no-native-http.spec.js +2 -2
- package/dist/__tests__/rules/no-parent-imports.spec.js +2 -2
- package/dist/__tests__/rules/no-restricted-globals.spec.js +101 -2
- package/dist/__tests__/utils/ast-utils.spec.js +1 -1
- package/dist/__tests__/utils/extension-point-utils.spec.js +1 -1
- package/dist/__tests__/utils/import-utils.spec.js +1 -1
- package/dist/__tests__/utils/jsx-utils.spec.js +1 -1
- package/dist/__tests__/utils/scope-utils.spec.js +1 -1
- package/dist/index.js +15 -12
- package/dist/rules/no-browser-dialogs.js +2 -2
- package/dist/rules/no-browser-storage.js +2 -2
- package/dist/rules/no-console.js +2 -2
- package/dist/rules/no-dom-access.js +2 -2
- package/dist/rules/no-experimental-imports.js +1 -1
- package/dist/rules/no-html-elements.js +1 -1
- package/dist/rules/no-import-meta-env.d.ts +2 -0
- package/dist/rules/no-import-meta-env.js +67 -0
- package/dist/rules/no-invalid-extension-point-imports.js +2 -2
- package/dist/rules/no-invalid-image-src.js +2 -2
- package/dist/rules/no-native-http.js +2 -2
- package/dist/rules/no-parent-imports.js +2 -2
- package/dist/rules/no-restricted-globals.js +4 -2
- package/dist/utils/extension-point-utils.js +1 -1
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -182,6 +182,7 @@ export default defineConfig([
|
|
|
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
183
|
| [no-experimental-imports](docs/rules/no-experimental-imports.md) | Warn when importing from the experimental path of @hubspot/ui-extensions. | |
|
|
184
184
|
| [no-html-elements](docs/rules/no-html-elements.md) | Disallow usage of unsupported HTML elements in UI Extensions. | |
|
|
185
|
+
| [no-import-meta-env](docs/rules/no-import-meta-env.md) | Prevent reading unsupported import.meta.env keys in UI Extensions builds. | |
|
|
185
186
|
| [no-invalid-extension-point-imports](docs/rules/no-invalid-extension-point-imports.md) | Prevent importing components from unsupported extension points. | |
|
|
186
187
|
| [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. | 🔧 |
|
|
187
188
|
| [no-native-http](docs/rules/no-native-http.md) | Prevent usage of native browser HTTP APIs (fetch and XMLHttpRequest) in UI Extensions. | |
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ESLint } from 'eslint';
|
|
2
|
-
import { config } from
|
|
2
|
+
import { config } from '../index.js';
|
|
3
3
|
/**
|
|
4
4
|
* A helper function to lint code with the imported ESLint config.
|
|
5
5
|
* Returns the linting messages.
|
|
@@ -300,4 +300,25 @@ describe('config', () => {
|
|
|
300
300
|
expect(customRuleError).toBeUndefined();
|
|
301
301
|
});
|
|
302
302
|
});
|
|
303
|
+
describe('custom no-import-meta-env rule', () => {
|
|
304
|
+
it('should error when reading an unsupported import.meta.env key', async () => {
|
|
305
|
+
const code = `
|
|
306
|
+
const apiUrl = import.meta.env.VITE_API_URL;
|
|
307
|
+
`;
|
|
308
|
+
const messages = await lintCode(code, 'test.js');
|
|
309
|
+
const customRuleError = messages.find((m) => m.ruleId === '@hubspot/ui-extensions/no-import-meta-env');
|
|
310
|
+
expect(customRuleError).toBeDefined();
|
|
311
|
+
expect(customRuleError?.severity).toBe(2);
|
|
312
|
+
expect(customRuleError?.message).toContain('VITE_API_URL');
|
|
313
|
+
});
|
|
314
|
+
it('should allow reading always-defined import.meta.env keys', async () => {
|
|
315
|
+
const code = `
|
|
316
|
+
const isDev = import.meta.env.DEV;
|
|
317
|
+
const mode = import.meta.env.MODE;
|
|
318
|
+
`;
|
|
319
|
+
const messages = await lintCode(code, 'test.js');
|
|
320
|
+
const customRuleError = messages.find((m) => m.ruleId === '@hubspot/ui-extensions/no-import-meta-env');
|
|
321
|
+
expect(customRuleError).toBeUndefined();
|
|
322
|
+
});
|
|
323
|
+
});
|
|
303
324
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RuleTester } from 'eslint';
|
|
2
2
|
import tseslint from 'typescript-eslint';
|
|
3
|
-
import { noBrowserDialogs } from
|
|
4
|
-
import { languageOptions } from
|
|
3
|
+
import { noBrowserDialogs } from '../../rules/no-browser-dialogs.js';
|
|
4
|
+
import { languageOptions } from '../../runtime.js';
|
|
5
5
|
const ruleTester = new RuleTester({
|
|
6
6
|
languageOptions: {
|
|
7
7
|
...languageOptions,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RuleTester } from 'eslint';
|
|
2
2
|
import tseslint from 'typescript-eslint';
|
|
3
|
-
import { noBrowserStorage } from
|
|
4
|
-
import { languageOptions } from
|
|
3
|
+
import { noBrowserStorage } from '../../rules/no-browser-storage.js';
|
|
4
|
+
import { languageOptions } from '../../runtime.js';
|
|
5
5
|
const ruleTester = new RuleTester({
|
|
6
6
|
languageOptions: {
|
|
7
7
|
...languageOptions,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RuleTester } from 'eslint';
|
|
2
2
|
import tseslint from 'typescript-eslint';
|
|
3
|
-
import { noConsole } from
|
|
4
|
-
import { languageOptions } from
|
|
3
|
+
import { noConsole } from '../../rules/no-console.js';
|
|
4
|
+
import { languageOptions } from '../../runtime.js';
|
|
5
5
|
const ruleTester = new RuleTester({
|
|
6
6
|
languageOptions: {
|
|
7
7
|
...languageOptions,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RuleTester } from 'eslint';
|
|
2
2
|
import tseslint from 'typescript-eslint';
|
|
3
|
-
import { noDomAccess } from
|
|
4
|
-
import { languageOptions } from
|
|
3
|
+
import { noDomAccess } from '../../rules/no-dom-access.js';
|
|
4
|
+
import { languageOptions } from '../../runtime.js';
|
|
5
5
|
const ruleTester = new RuleTester({
|
|
6
6
|
languageOptions: {
|
|
7
7
|
...languageOptions,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RuleTester } from 'eslint';
|
|
2
|
-
import { noExperimentalImports } from
|
|
3
|
-
import { languageOptions } from
|
|
2
|
+
import { noExperimentalImports } from '../../rules/no-experimental-imports.js';
|
|
3
|
+
import { languageOptions } from '../../runtime.js';
|
|
4
4
|
const ruleTesterOptions = {
|
|
5
5
|
languageOptions: {
|
|
6
6
|
...languageOptions,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RuleTester } from 'eslint';
|
|
2
|
-
import { unsupportedHtmlElements } from
|
|
3
|
-
import { languageOptions } from
|
|
2
|
+
import { unsupportedHtmlElements } from '../../rules/no-html-elements.js';
|
|
3
|
+
import { languageOptions } from '../../runtime.js';
|
|
4
4
|
const ruleTester = new RuleTester({
|
|
5
5
|
languageOptions: {
|
|
6
6
|
...languageOptions,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { RuleTester } from 'eslint';
|
|
2
|
+
import { noImportMetaEnv } from '../../rules/no-import-meta-env.js';
|
|
3
|
+
import { languageOptions } from '../../runtime.js';
|
|
4
|
+
const ruleTester = new RuleTester({
|
|
5
|
+
languageOptions: {
|
|
6
|
+
...languageOptions,
|
|
7
|
+
parserOptions: {
|
|
8
|
+
ecmaVersion: 'latest',
|
|
9
|
+
sourceType: 'module',
|
|
10
|
+
ecmaFeatures: {
|
|
11
|
+
jsx: true,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
describe('no-import-meta-env', () => {
|
|
17
|
+
ruleTester.run('no-import-meta-env', noImportMetaEnv, {
|
|
18
|
+
valid: [
|
|
19
|
+
{
|
|
20
|
+
name: 'allows import.meta.env.MODE',
|
|
21
|
+
code: `const mode = import.meta.env.MODE;`,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'allows import.meta.env.DEV',
|
|
25
|
+
code: `const isDev = import.meta.env.DEV;`,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'allows import.meta.env.PROD',
|
|
29
|
+
code: `const isProd = import.meta.env.PROD;`,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'allows import.meta.env.SSR',
|
|
33
|
+
code: `const isSsr = import.meta.env.SSR;`,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'allows import.meta.env.BASE_URL',
|
|
37
|
+
code: `const base = import.meta.env.BASE_URL;`,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'allows bracket access for allowed keys',
|
|
41
|
+
code: `const mode = import.meta.env['MODE'];`,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'allows import.meta.env itself (no property access)',
|
|
45
|
+
code: `const env = import.meta.env;`,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'allows import.meta without env',
|
|
49
|
+
code: `const url = import.meta.url;`,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
invalid: [
|
|
53
|
+
{
|
|
54
|
+
name: 'flags dot access for custom VITE_ key',
|
|
55
|
+
code: `const apiKey = import.meta.env.VITE_API_KEY;`,
|
|
56
|
+
errors: [
|
|
57
|
+
{ messageId: 'unsupportedKey', data: { key: 'VITE_API_KEY' } },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'flags bracket access for custom VITE_ key',
|
|
62
|
+
code: `const apiKey = import.meta.env['VITE_API_KEY'];`,
|
|
63
|
+
errors: [
|
|
64
|
+
{ messageId: 'unsupportedKey', data: { key: 'VITE_API_KEY' } },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'flags dynamic computed access',
|
|
69
|
+
code: `const key = 'VITE_FOO'; const val = import.meta.env[key];`,
|
|
70
|
+
errors: [{ messageId: 'dynamicAccess' }],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'flags multiple unsupported keys in one file',
|
|
74
|
+
code: `
|
|
75
|
+
const a = import.meta.env.VITE_API_URL;
|
|
76
|
+
const b = import.meta.env.VITE_SECRET;
|
|
77
|
+
`,
|
|
78
|
+
errors: [
|
|
79
|
+
{ messageId: 'unsupportedKey', data: { key: 'VITE_API_URL' } },
|
|
80
|
+
{ messageId: 'unsupportedKey', data: { key: 'VITE_SECRET' } },
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'flags unsupported key in JSX expression',
|
|
85
|
+
code: `
|
|
86
|
+
import { Text } from '@hubspot/ui-extensions';
|
|
87
|
+
function Extension() {
|
|
88
|
+
return <Text>{import.meta.env.VITE_LABEL}</Text>;
|
|
89
|
+
}
|
|
90
|
+
`,
|
|
91
|
+
errors: [{ messageId: 'unsupportedKey', data: { key: 'VITE_LABEL' } }],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'flags bracket access for a non-string expression',
|
|
95
|
+
code: `const val = import.meta.env[getKey()];`,
|
|
96
|
+
errors: [{ messageId: 'dynamicAccess' }],
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RuleTester } from 'eslint';
|
|
2
|
-
import { noInvalidExtensionPointImports } from
|
|
3
|
-
import { languageOptions } from
|
|
2
|
+
import { noInvalidExtensionPointImports } from '../../rules/no-invalid-extension-point-imports.js';
|
|
3
|
+
import { languageOptions } from '../../runtime.js';
|
|
4
4
|
const ruleTesterOptions = {
|
|
5
5
|
languageOptions: {
|
|
6
6
|
...languageOptions,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RuleTester } from 'eslint';
|
|
2
|
-
import { noInvalidImageSrc } from
|
|
3
|
-
import { languageOptions } from
|
|
2
|
+
import { noInvalidImageSrc } from '../../rules/no-invalid-image-src.js';
|
|
3
|
+
import { languageOptions } from '../../runtime.js';
|
|
4
4
|
const ruleTester = new RuleTester({
|
|
5
5
|
languageOptions: {
|
|
6
6
|
...languageOptions,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RuleTester } from 'eslint';
|
|
2
2
|
import tseslint from 'typescript-eslint';
|
|
3
|
-
import { noNativeHttp } from
|
|
4
|
-
import { languageOptions } from
|
|
3
|
+
import { noNativeHttp } from '../../rules/no-native-http.js';
|
|
4
|
+
import { languageOptions } from '../../runtime.js';
|
|
5
5
|
const ruleTester = new RuleTester({
|
|
6
6
|
languageOptions: {
|
|
7
7
|
...languageOptions,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RuleTester } from 'eslint';
|
|
2
|
-
import { noParentImports } from
|
|
3
|
-
import { languageOptions } from
|
|
2
|
+
import { noParentImports } from '../../rules/no-parent-imports.js';
|
|
3
|
+
import { languageOptions } from '../../runtime.js';
|
|
4
4
|
const ruleTesterOptions = {
|
|
5
5
|
languageOptions: {
|
|
6
6
|
...languageOptions,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RuleTester } from 'eslint';
|
|
2
2
|
import tseslint from 'typescript-eslint';
|
|
3
|
-
import { noRestrictedGlobals } from
|
|
4
|
-
import { languageOptions } from
|
|
3
|
+
import { noRestrictedGlobals } from '../../rules/no-restricted-globals.js';
|
|
4
|
+
import { languageOptions } from '../../runtime.js';
|
|
5
5
|
const ruleTester = new RuleTester({
|
|
6
6
|
languageOptions: {
|
|
7
7
|
...languageOptions,
|
|
@@ -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 { getMemberExpressionPropertyName, isGlobalIdentifierCall, isWindowSelfOrGlobalThisPropertyAccess, } from
|
|
1
|
+
import { getMemberExpressionPropertyName, isGlobalIdentifierCall, isWindowSelfOrGlobalThisPropertyAccess, } from '../../utils/ast-utils.js';
|
|
2
2
|
describe('ast-utils', () => {
|
|
3
3
|
describe('isGlobalIdentifierCall', () => {
|
|
4
4
|
it('should return true for global identifier calls', () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getExtensionPointFromFilePath, getHubspotImportExtensionPoint, isImportOutsideExtensionPoint, mapExtensionPointToImportDirectory, } from
|
|
1
|
+
import { getExtensionPointFromFilePath, getHubspotImportExtensionPoint, isImportOutsideExtensionPoint, mapExtensionPointToImportDirectory, } from '../../utils/extension-point-utils.js';
|
|
2
2
|
describe('extension-point-utils', () => {
|
|
3
3
|
describe('getExtensionPointFromFilePath', () => {
|
|
4
4
|
it('should return the extension point for valid paths', () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getImportedComponentName, isExperimentalImport, isHubspotExtensibilityImport, isRelativeImport, } from
|
|
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', () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { findJSXAttribute, isJSXComponent } from
|
|
1
|
+
import { findJSXAttribute, isJSXComponent } from '../../utils/jsx-utils.js';
|
|
2
2
|
describe('jsx-utils', () => {
|
|
3
3
|
describe('findJSXAttribute', () => {
|
|
4
4
|
it('should find an attribute by name', () => {
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import globals from 'globals';
|
|
2
|
-
import { noBrowserDialogs } from
|
|
3
|
-
import { noBrowserStorage } from
|
|
4
|
-
import { noConsole } from
|
|
5
|
-
import { noDomAccess } from
|
|
6
|
-
import { noExperimentalImports } from
|
|
7
|
-
import { unsupportedHtmlElements } from
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
2
|
+
import { noBrowserDialogs } from './rules/no-browser-dialogs.js';
|
|
3
|
+
import { noBrowserStorage } from './rules/no-browser-storage.js';
|
|
4
|
+
import { noConsole } from './rules/no-console.js';
|
|
5
|
+
import { noDomAccess } from './rules/no-dom-access.js';
|
|
6
|
+
import { noExperimentalImports } from './rules/no-experimental-imports.js';
|
|
7
|
+
import { unsupportedHtmlElements } from './rules/no-html-elements.js';
|
|
8
|
+
import { noImportMetaEnv } from './rules/no-import-meta-env.js';
|
|
9
|
+
import { noInvalidExtensionPointImports } from './rules/no-invalid-extension-point-imports.js';
|
|
10
|
+
import { noInvalidImageSrc } from './rules/no-invalid-image-src.js';
|
|
11
|
+
import { noNativeHttp } from './rules/no-native-http.js';
|
|
12
|
+
import { noParentImports } from './rules/no-parent-imports.js';
|
|
13
|
+
import { noRestrictedGlobals } from './rules/no-restricted-globals.js';
|
|
14
|
+
import { languageOptions } from './runtime.js';
|
|
14
15
|
const rules = {
|
|
15
16
|
'no-html-elements': unsupportedHtmlElements,
|
|
16
17
|
'no-invalid-extension-point-imports': noInvalidExtensionPointImports,
|
|
@@ -23,6 +24,7 @@ const rules = {
|
|
|
23
24
|
'no-browser-storage': noBrowserStorage,
|
|
24
25
|
'no-parent-imports': noParentImports,
|
|
25
26
|
'no-restricted-globals': noRestrictedGlobals,
|
|
27
|
+
'no-import-meta-env': noImportMetaEnv,
|
|
26
28
|
};
|
|
27
29
|
// We export a config array for our users to extend
|
|
28
30
|
export const config = [
|
|
@@ -62,6 +64,7 @@ export const config = [
|
|
|
62
64
|
'@hubspot/ui-extensions/no-browser-storage': 'error',
|
|
63
65
|
'@hubspot/ui-extensions/no-parent-imports': 'error',
|
|
64
66
|
'@hubspot/ui-extensions/no-restricted-globals': 'error',
|
|
67
|
+
'@hubspot/ui-extensions/no-import-meta-env': 'error',
|
|
65
68
|
},
|
|
66
69
|
},
|
|
67
70
|
];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getMemberExpressionPropertyName, isGlobalIdentifierCall, isWindowSelfOrGlobalThisPropertyAccess, } from
|
|
2
|
-
import { isInTypeContext, isLocalVariable } from
|
|
1
|
+
import { getMemberExpressionPropertyName, isGlobalIdentifierCall, isWindowSelfOrGlobalThisPropertyAccess, } from '../utils/ast-utils.js';
|
|
2
|
+
import { isInTypeContext, isLocalVariable } from '../utils/scope-utils.js';
|
|
3
3
|
const MESSAGE_IDS = {
|
|
4
4
|
alert: 'nativeAlert',
|
|
5
5
|
confirm: 'nativeConfirm',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { isWindowSelfOrGlobalThisPropertyAccess } from
|
|
2
|
-
import { isInTypeContext, isLocalVariable } from
|
|
1
|
+
import { isWindowSelfOrGlobalThisPropertyAccess } from '../utils/ast-utils.js';
|
|
2
|
+
import { isInTypeContext, isLocalVariable } from '../utils/scope-utils.js';
|
|
3
3
|
const STORAGE_IDENTIFIERS = new Set([
|
|
4
4
|
'localStorage',
|
|
5
5
|
'sessionStorage',
|
package/dist/rules/no-console.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getMemberExpressionPropertyName } from
|
|
2
|
-
import { isInTypeContext, isLocalVariable } from
|
|
1
|
+
import { getMemberExpressionPropertyName } from '../utils/ast-utils.js';
|
|
2
|
+
import { isInTypeContext, isLocalVariable } from '../utils/scope-utils.js';
|
|
3
3
|
const CONSOLE_METHODS = ['log', 'info', 'warn', 'debug', 'error'];
|
|
4
4
|
const LOGGER_ALTERNATIVES = {
|
|
5
5
|
log: 'logger.debug()',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { isWindowSelfOrGlobalThisPropertyAccess } from
|
|
2
|
-
import { isInTypeContext, isLocalVariable } from
|
|
1
|
+
import { isWindowSelfOrGlobalThisPropertyAccess } from '../utils/ast-utils.js';
|
|
2
|
+
import { isInTypeContext, isLocalVariable } from '../utils/scope-utils.js';
|
|
3
3
|
export const noDomAccess = {
|
|
4
4
|
meta: {
|
|
5
5
|
type: 'problem',
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Vite's always-defined import.meta.env members
|
|
2
|
+
const ALLOWED_KEYS = new Set(['MODE', 'DEV', 'PROD', 'SSR', 'BASE_URL']);
|
|
3
|
+
function isImportMetaEnv(node) {
|
|
4
|
+
const obj = node.object;
|
|
5
|
+
if (obj.type !== 'MemberExpression')
|
|
6
|
+
return false;
|
|
7
|
+
const meta = obj.object;
|
|
8
|
+
const envProp = obj.property;
|
|
9
|
+
return (meta.type === 'MetaProperty' &&
|
|
10
|
+
meta.meta.name === 'import' &&
|
|
11
|
+
meta.property.name === 'meta' &&
|
|
12
|
+
envProp.type === 'Identifier' &&
|
|
13
|
+
envProp.name === 'env' &&
|
|
14
|
+
!obj.computed);
|
|
15
|
+
}
|
|
16
|
+
export const noImportMetaEnv = {
|
|
17
|
+
meta: {
|
|
18
|
+
type: 'problem',
|
|
19
|
+
docs: {
|
|
20
|
+
description: 'Prevent reading unsupported import.meta.env keys in UI Extensions builds.',
|
|
21
|
+
url: 'https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensibility/linting/rules/no-import-meta-env',
|
|
22
|
+
},
|
|
23
|
+
messages: {
|
|
24
|
+
unsupportedKey: '"import.meta.env.{{key}}" is not available in UI Extensions builds. Only MODE, DEV, PROD, SSR, and BASE_URL are always defined.',
|
|
25
|
+
dynamicAccess: 'Dynamic access of import.meta.env is not supported in UI Extensions builds. Only MODE, DEV, PROD, SSR, and BASE_URL are always defined.',
|
|
26
|
+
},
|
|
27
|
+
schema: [],
|
|
28
|
+
},
|
|
29
|
+
create(context) {
|
|
30
|
+
return {
|
|
31
|
+
MemberExpression(node) {
|
|
32
|
+
if (!isImportMetaEnv(node))
|
|
33
|
+
return;
|
|
34
|
+
const prop = node.property;
|
|
35
|
+
if (!node.computed) {
|
|
36
|
+
// import.meta.env.KEY
|
|
37
|
+
const key = prop.name;
|
|
38
|
+
if (!ALLOWED_KEYS.has(key)) {
|
|
39
|
+
context.report({
|
|
40
|
+
node: node,
|
|
41
|
+
messageId: 'unsupportedKey',
|
|
42
|
+
data: { key },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else if (prop.type === 'Literal' && typeof prop.value === 'string') {
|
|
47
|
+
// import.meta.env['KEY']
|
|
48
|
+
const key = prop.value;
|
|
49
|
+
if (!ALLOWED_KEYS.has(key)) {
|
|
50
|
+
context.report({
|
|
51
|
+
node: node,
|
|
52
|
+
messageId: 'unsupportedKey',
|
|
53
|
+
data: { key },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
// import.meta.env[dynamicKey]
|
|
59
|
+
context.report({
|
|
60
|
+
node: node,
|
|
61
|
+
messageId: 'dynamicAccess',
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import { getExtensionPointFromFilePath, getHubspotImportExtensionPoint, mapExtensionPointToImportDirectory, } from
|
|
3
|
-
import { isHubspotExtensibilityImport } from
|
|
2
|
+
import { getExtensionPointFromFilePath, getHubspotImportExtensionPoint, mapExtensionPointToImportDirectory, } from '../utils/extension-point-utils.js';
|
|
3
|
+
import { isHubspotExtensibilityImport } from '../utils/import-utils.js';
|
|
4
4
|
export const noInvalidExtensionPointImports = {
|
|
5
5
|
meta: {
|
|
6
6
|
type: 'problem',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getImportedComponentName, isHubspotExtensibilityImport, } from
|
|
2
|
-
import { findJSXAttribute, isJSXComponent } from
|
|
1
|
+
import { getImportedComponentName, isHubspotExtensibilityImport, } from '../utils/import-utils.js';
|
|
2
|
+
import { findJSXAttribute, isJSXComponent } from '../utils/jsx-utils.js';
|
|
3
3
|
const ALLOWED_PROTOCOLS = {
|
|
4
4
|
HTTP: 'http:',
|
|
5
5
|
HTTPS: 'https:',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { isGlobalIdentifierCall, isWindowSelfOrGlobalThisPropertyAccess, } from
|
|
2
|
-
import { isInTypeContext, isLocalVariable } from
|
|
1
|
+
import { isGlobalIdentifierCall, isWindowSelfOrGlobalThisPropertyAccess, } from '../utils/ast-utils.js';
|
|
2
|
+
import { isInTypeContext, isLocalVariable } from '../utils/scope-utils.js';
|
|
3
3
|
export const noNativeHttp = {
|
|
4
4
|
meta: {
|
|
5
5
|
type: 'problem',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getExtensionPointFromFilePath, isImportOutsideExtensionPoint, } from
|
|
2
|
-
import { isRelativeImport } from
|
|
1
|
+
import { getExtensionPointFromFilePath, isImportOutsideExtensionPoint, } from '../utils/extension-point-utils.js';
|
|
2
|
+
import { isRelativeImport } from '../utils/import-utils.js';
|
|
3
3
|
export const noParentImports = {
|
|
4
4
|
meta: {
|
|
5
5
|
type: 'problem',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { isGlobalIdentifierCall, isWindowSelfOrGlobalThisPropertyAccess, } from
|
|
2
|
-
import { isInTypeContext, isLocalVariable } from
|
|
1
|
+
import { isGlobalIdentifierCall, isWindowSelfOrGlobalThisPropertyAccess, } from '../utils/ast-utils.js';
|
|
2
|
+
import { isInTypeContext, isLocalVariable } from '../utils/scope-utils.js';
|
|
3
3
|
// Function-like globals that should only be checked as calls
|
|
4
4
|
const RESTRICTED_FUNCTION_GLOBALS = new Set([
|
|
5
5
|
'importScripts',
|
|
@@ -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
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import { EXTENSION_IMPORT_DIRECTORIES, EXTENSION_POINT_DIRECTORIES, } from
|
|
2
|
+
import { EXTENSION_IMPORT_DIRECTORIES, EXTENSION_POINT_DIRECTORIES, } from '../constants.js';
|
|
3
3
|
/**
|
|
4
4
|
* This function takes a file path as input and returns the extension point.
|
|
5
5
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hubspot/eslint-config-ui-extensions",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "eslint plugins for HubSpot React-based UI Extensions",
|
|
6
6
|
"keywords": [
|
|
@@ -20,13 +20,14 @@
|
|
|
20
20
|
"globals": "13.24.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@eslint/js": "9.39.
|
|
23
|
+
"@eslint/js": "9.39.3",
|
|
24
24
|
"@types/eslint": "9.6.1",
|
|
25
25
|
"@types/estree-jsx": "1.0.5",
|
|
26
|
-
"@types/node": "
|
|
26
|
+
"@types/node": "24.13.3",
|
|
27
|
+
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
27
28
|
"@vitest/coverage-v8": "4.0.18",
|
|
28
29
|
"@vitest/eslint-plugin": "1.6.6",
|
|
29
|
-
"eslint": "9.39.
|
|
30
|
+
"eslint": "9.39.3",
|
|
30
31
|
"eslint-doc-generator": "2.3.0",
|
|
31
32
|
"eslint-plugin-eslint-plugin": "5.5.1",
|
|
32
33
|
"eslint-plugin-node": "11.1.0",
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
"license": "MIT",
|
|
53
54
|
"gitHead": "36d3a36ffaa5bde1a7b73dc139d75027aae65b7f",
|
|
54
55
|
"scripts": {
|
|
55
|
-
"build": "pnpm run clean &&
|
|
56
|
+
"build": "pnpm run clean && tsgo",
|
|
56
57
|
"clean": "rm -rf dist/",
|
|
57
58
|
"format": "prettier --write . --ignore-path ../../.prettierignore",
|
|
58
59
|
"format:check": "prettier --check . --ignore-path ../../.prettierignore",
|
|
@@ -64,6 +65,6 @@
|
|
|
64
65
|
"test:coverage": "vitest run --coverage",
|
|
65
66
|
"test:watch": "vitest",
|
|
66
67
|
"update:eslint-docs": "eslint-doc-generator",
|
|
67
|
-
"watch": "
|
|
68
|
+
"watch": "tsgo --watch"
|
|
68
69
|
}
|
|
69
70
|
}
|