@hubspot/eslint-config-ui-extensions 0.12.0 → 1.1.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.
Files changed (78) hide show
  1. package/README.md +168 -24
  2. package/dist/__tests__/index.spec.d.ts +1 -0
  3. package/dist/__tests__/index.spec.js +303 -0
  4. package/dist/__tests__/rules/no-browser-dialogs.spec.d.ts +1 -0
  5. package/dist/__tests__/rules/no-browser-dialogs.spec.js +263 -0
  6. package/dist/__tests__/rules/no-browser-storage.spec.d.ts +1 -0
  7. package/dist/__tests__/rules/no-browser-storage.spec.js +428 -0
  8. package/dist/__tests__/rules/no-console.spec.d.ts +1 -0
  9. package/dist/__tests__/rules/no-console.spec.js +262 -0
  10. package/dist/__tests__/rules/no-dom-access.spec.d.ts +1 -0
  11. package/dist/__tests__/rules/no-dom-access.spec.js +288 -0
  12. package/dist/__tests__/rules/no-experimental-imports.spec.d.ts +1 -0
  13. package/dist/__tests__/rules/no-experimental-imports.spec.js +67 -0
  14. package/dist/__tests__/rules/no-html-elements.spec.d.ts +1 -0
  15. package/dist/__tests__/rules/no-html-elements.spec.js +142 -0
  16. package/dist/__tests__/rules/no-invalid-extension-point-imports.spec.d.ts +1 -0
  17. package/dist/__tests__/rules/no-invalid-extension-point-imports.spec.js +269 -0
  18. package/dist/__tests__/rules/no-invalid-image-src.spec.d.ts +1 -0
  19. package/dist/__tests__/rules/no-invalid-image-src.spec.js +227 -0
  20. package/dist/__tests__/rules/no-native-http.spec.d.ts +1 -0
  21. package/dist/__tests__/rules/no-native-http.spec.js +596 -0
  22. package/dist/__tests__/rules/no-parent-imports.spec.d.ts +1 -0
  23. package/dist/__tests__/rules/no-parent-imports.spec.js +337 -0
  24. package/dist/__tests__/rules/no-restricted-globals.spec.d.ts +1 -0
  25. package/dist/__tests__/rules/no-restricted-globals.spec.js +434 -0
  26. package/dist/__tests__/utils/ast-utils.spec.d.ts +1 -0
  27. package/dist/__tests__/utils/ast-utils.spec.js +297 -0
  28. package/dist/__tests__/utils/extension-point-utils.spec.d.ts +1 -0
  29. package/dist/__tests__/utils/extension-point-utils.spec.js +125 -0
  30. package/dist/__tests__/utils/import-utils.spec.d.ts +1 -0
  31. package/dist/__tests__/utils/import-utils.spec.js +176 -0
  32. package/dist/__tests__/utils/jsx-utils.spec.d.ts +1 -0
  33. package/dist/__tests__/utils/jsx-utils.spec.js +120 -0
  34. package/dist/__tests__/utils/scope-utils.spec.d.ts +1 -0
  35. package/dist/__tests__/utils/scope-utils.spec.js +80 -0
  36. package/dist/constants.d.ts +3 -0
  37. package/dist/constants.js +124 -0
  38. package/dist/index.d.ts +4 -0
  39. package/dist/index.js +75 -0
  40. package/dist/rules/no-browser-dialogs.d.ts +2 -0
  41. package/dist/rules/no-browser-dialogs.js +62 -0
  42. package/dist/rules/no-browser-storage.d.ts +2 -0
  43. package/dist/rules/no-browser-storage.js +78 -0
  44. package/dist/rules/no-console.d.ts +2 -0
  45. package/dist/rules/no-console.js +78 -0
  46. package/dist/rules/no-dom-access.d.ts +2 -0
  47. package/dist/rules/no-dom-access.js +63 -0
  48. package/dist/rules/no-experimental-imports.d.ts +2 -0
  49. package/dist/rules/no-experimental-imports.js +29 -0
  50. package/dist/rules/no-html-elements.d.ts +2 -0
  51. package/dist/rules/no-html-elements.js +39 -0
  52. package/dist/rules/no-invalid-extension-point-imports.d.ts +2 -0
  53. package/dist/rules/no-invalid-extension-point-imports.js +54 -0
  54. package/dist/rules/no-invalid-image-src.d.ts +2 -0
  55. package/dist/rules/no-invalid-image-src.js +120 -0
  56. package/dist/rules/no-native-http.d.ts +2 -0
  57. package/dist/rules/no-native-http.js +70 -0
  58. package/dist/rules/no-parent-imports.d.ts +2 -0
  59. package/dist/rules/no-parent-imports.js +46 -0
  60. package/dist/rules/no-restricted-globals.d.ts +2 -0
  61. package/dist/rules/no-restricted-globals.js +132 -0
  62. package/dist/runtime.d.ts +2 -0
  63. package/dist/runtime.js +13 -0
  64. package/dist/utils/ast-utils.d.ts +42 -0
  65. package/dist/utils/ast-utils.js +79 -0
  66. package/dist/utils/extension-point-utils.d.ts +57 -0
  67. package/dist/utils/extension-point-utils.js +108 -0
  68. package/dist/utils/import-utils.d.ts +34 -0
  69. package/dist/utils/import-utils.js +52 -0
  70. package/dist/utils/jsx-utils.d.ts +30 -0
  71. package/dist/utils/jsx-utils.js +40 -0
  72. package/dist/utils/scope-utils.d.ts +30 -0
  73. package/dist/utils/scope-utils.js +66 -0
  74. package/package.json +43 -24
  75. package/CHANGELOG.md +0 -51
  76. package/eslintrc.js +0 -12
  77. package/lib/index.js +0 -57
  78. package/lib/runtime.js +0 -17
package/README.md CHANGED
@@ -1,47 +1,191 @@
1
1
  # @hubspot/eslint-config-ui-extensions
2
2
 
3
- [eslint shareable configs](https://eslint.org/docs/latest/extend/shareable-configs) for building UI Extensions on HubSpot with React.
3
+ [ESLint shareable config](https://eslint.org/docs/latest/extend/shareable-configs) for building UI Extensions on HubSpot with React.
4
+
5
+ This package provides ESLint rules specifically designed for the HubSpot UI Extensions environment. It catches common issues like using unavailable browser APIs, incorrect imports, and other patterns that won't work in the sandboxed web worker context.
4
6
 
5
7
  ## Installation
6
8
 
7
- You'll first need to install [ESLint](https://eslint.org/) and alongside this package:
9
+ > **Upgrading from v0.x?** See the [Migration Guide](./MIGRATION.md) for step-by-step instructions.
10
+
11
+ Install [ESLint](https://eslint.org/) 9+ alongside this package:
8
12
 
9
13
  ```sh
10
14
  npm i --save-dev eslint @hubspot/eslint-config-ui-extensions
11
15
  ```
12
16
 
17
+ ## Basic Usage
13
18
 
14
- ## Usage
15
-
16
- Create an .eslintrc.js file in the root of your project:
19
+ Create an `eslint.config.js` file in the root of your project:
17
20
 
18
21
  ```js
19
- module.exports = {
20
- extends: "@hubspot/ui-extensions",
21
- ignorePatterns: ["node_modules", "dist"],
22
- };
22
+ import { config } from '@hubspot/eslint-config-ui-extensions';
23
+
24
+ export default [
25
+ ...config,
26
+ // Your project-specific overrides
27
+ ];
28
+ ```
29
+
30
+ Make sure your `package.json` has `"type": "module"` set, then add a lint script:
31
+
32
+ ```json
33
+ {
34
+ "type": "module",
35
+ "scripts": {
36
+ "lint": "eslint ."
37
+ }
38
+ }
23
39
  ```
24
40
 
25
- For those using TypeScript, we'll likely expand this package to include a base setup in the near future, but in the meantime you can start here and follow these steps to get TS support:
41
+ Run `npm run lint` to check for linting issues.
42
+
43
+ ## Recommended Setup
44
+
45
+ The basic config above only includes HubSpot UI Extensions-specific rules. For a production-ready setup, we recommend adding standard ESLint rules, TypeScript support, React Hooks linting, Prettier compatibility, and unused import detection.
46
+
47
+ ### Install Additional Dependencies
26
48
 
27
- - `npm i --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin`
28
- - Adapt your lint script in package.json (if applicable) to make sure you're targeting your source files, perhaps something like `"lint": "npx eslint . --ext .ts,.tsx"`
29
- - Update your eslintrc config:
49
+ ```sh
50
+ npm i --save-dev @eslint/js typescript-eslint eslint-plugin-react eslint-plugin-react-hooks eslint-config-prettier eslint-plugin-unused-imports
51
+ ```
52
+
53
+ ### Full Recommended Config
30
54
 
31
- ```diff
32
- -extends: ['@hubspot/ui-extensions'],
33
- +extends: ['@hubspot/ui-extensions', 'plugin:@typescript-eslint/recommended'],
34
- +plugins: ['@typescript-eslint'],
55
+ ```js
56
+ import { defineConfig } from 'eslint/config';
57
+ import js from '@eslint/js';
58
+ import tseslint from 'typescript-eslint';
59
+ import react from 'eslint-plugin-react';
60
+ import reactHooks from 'eslint-plugin-react-hooks';
61
+ import unusedImports from 'eslint-plugin-unused-imports';
62
+ import prettier from 'eslint-config-prettier';
63
+ import { config as uiExtensionsConfig } from '@hubspot/eslint-config-ui-extensions';
64
+
65
+ export default defineConfig([
66
+ // Standard ESLint recommended rules
67
+ js.configs.recommended,
68
+
69
+ // TypeScript support with stylistic rules
70
+ tseslint.configs.recommended,
71
+ tseslint.configs.stylistic,
72
+
73
+ // React recommended rules + JSX runtime support (React 17+)
74
+ react.configs.flat.recommended,
75
+ react.configs.flat['jsx-runtime'],
76
+ {
77
+ settings: {
78
+ react: { version: 'detect' },
79
+ },
80
+ },
81
+
82
+ // HubSpot UI Extensions rules
83
+ ...uiExtensionsConfig,
84
+
85
+ // React Hooks rules
86
+ reactHooks.configs.flat['recommended-latest'],
87
+
88
+ // Unused imports detection (auto-fixable with --fix)
89
+ {
90
+ plugins: {
91
+ 'unused-imports': unusedImports,
92
+ },
93
+ rules: {
94
+ 'no-unused-vars': 'off',
95
+ '@typescript-eslint/no-unused-vars': 'off',
96
+ 'unused-imports/no-unused-imports': 'error',
97
+ 'unused-imports/no-unused-vars': [
98
+ 'warn',
99
+ {
100
+ vars: 'all',
101
+ varsIgnorePattern: '^_',
102
+ args: 'after-used',
103
+ argsIgnorePattern: '^_',
104
+ },
105
+ ],
106
+ },
107
+ },
108
+
109
+ // Prettier compatibility (must be last to override other configs)
110
+ prettier,
111
+ ]);
35
112
  ```
36
113
 
37
- Running `npm run lint`, you should now see TypeScript-aware linting in your terminal output and it will likely automatically work inside your editor of choice as well (though some editors may require additional config steps).
114
+ ### What Each Addition Provides
38
115
 
116
+ | Package | Purpose |
117
+ | :------ | :------ |
118
+ | `@eslint/js` | ESLint's built-in recommended rules for catching common JavaScript issues |
119
+ | `typescript-eslint` | TypeScript parsing and linting rules (`stylistic` adds consistent code style enforcement) |
120
+ | `eslint-plugin-react` | React-specific rules; `jsx-runtime` config is for React 17+ (no need to import React in every file) |
121
+ | `eslint-plugin-react-hooks` | Enforces [Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks) and verifies dependency arrays |
122
+ | `eslint-config-prettier` | Disables ESLint rules that conflict with Prettier formatting |
123
+ | `eslint-plugin-unused-imports` | Auto-fixable detection and removal of unused imports |
39
124
 
40
- ## Rules
125
+ ### JavaScript-Only Projects
41
126
 
42
- This config extends the following configs, meaning you can customize the `rules` entry in your project-level config to include any rules from these packages:
127
+ If you're not using TypeScript, you can simplify the config:
128
+
129
+ ```js
130
+ import { defineConfig } from 'eslint/config';
131
+ import js from '@eslint/js';
132
+ import react from 'eslint-plugin-react';
133
+ import reactHooks from 'eslint-plugin-react-hooks';
134
+ import unusedImports from 'eslint-plugin-unused-imports';
135
+ import prettier from 'eslint-config-prettier';
136
+ import { config as uiExtensionsConfig } from '@hubspot/eslint-config-ui-extensions';
137
+
138
+ export default defineConfig([
139
+ js.configs.recommended,
140
+ react.configs.flat.recommended,
141
+ react.configs.flat['jsx-runtime'],
142
+ {
143
+ settings: {
144
+ react: { version: 'detect' },
145
+ },
146
+ },
147
+ ...uiExtensionsConfig,
148
+ reactHooks.configs.flat['recommended-latest'],
149
+ {
150
+ plugins: {
151
+ 'unused-imports': unusedImports,
152
+ },
153
+ rules: {
154
+ 'no-unused-vars': 'off',
155
+ 'unused-imports/no-unused-imports': 'error',
156
+ 'unused-imports/no-unused-vars': [
157
+ 'warn',
158
+ {
159
+ vars: 'all',
160
+ varsIgnorePattern: '^_',
161
+ args: 'after-used',
162
+ argsIgnorePattern: '^_',
163
+ },
164
+ ],
165
+ },
166
+ },
167
+ prettier,
168
+ ]);
169
+ ```
170
+
171
+ ## Rules
43
172
 
44
- - ['eslint:recommended'](https://eslint.org/docs/latest/rules/)
45
- - ['plugin:react/recommended'](https://www.npmjs.com/package/eslint-plugin-react)
46
- - ['plugin:react-hooks/recommended'](https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks)
47
- - ['prettier'](https://www.npmjs.com/package/eslint-config-prettier)
173
+ <!-- begin auto-generated rules list -->
174
+
175
+ 🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).
176
+
177
+ | Name                               | Description | 🔧 |
178
+ | :------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :- |
179
+ | [no-browser-dialogs](docs/rules/no-browser-dialogs.md) | Prevent usage of native browser dialog APIs (alert, confirm, and prompt) in UI Extensions. | |
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
+ | [no-console](docs/rules/no-console.md) | Prevent usage of console methods in UI Extensions in favor of the SDK logger utility. | |
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. | |
184
+ | [no-html-elements](docs/rules/no-html-elements.md) | Disallow usage of unsupported HTML elements in UI Extensions. | |
185
+ | [no-invalid-extension-point-imports](docs/rules/no-invalid-extension-point-imports.md) | Prevent importing components from unsupported extension points. | |
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. | 🔧 |
187
+ | [no-native-http](docs/rules/no-native-http.md) | Prevent usage of native browser HTTP APIs (fetch and XMLHttpRequest) in UI Extensions. | |
188
+ | [no-parent-imports](docs/rules/no-parent-imports.md) | Prevent importing files from outside the extension point root directory | |
189
+ | [no-restricted-globals](docs/rules/no-restricted-globals.md) | Prevent usage of browser globals that are not available in the sandboxed web worker environment. | |
190
+
191
+ <!-- end auto-generated rules list -->
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,303 @@
1
+ import { ESLint } from 'eslint';
2
+ import { config } from "../index.js";
3
+ /**
4
+ * A helper function to lint code with the imported ESLint config.
5
+ * Returns the linting messages.
6
+ */
7
+ async function lintCode(code, filePath, cwd) {
8
+ const eslint = new ESLint({
9
+ overrideConfigFile: true,
10
+ overrideConfig: config,
11
+ ...(cwd && { cwd }),
12
+ });
13
+ const results = await eslint.lintText(code, { filePath });
14
+ return results[0].messages;
15
+ }
16
+ /**
17
+ * Config regression tests
18
+ *
19
+ * These tests ensure we don't accidentally break the exported config by:
20
+ * - Verifying critical rules are present and fire correctly
21
+ * - Validating the config structure loads without errors
22
+ *
23
+ * For imported rules, we simply want to make sure they are registered correctly.
24
+ * The maintainer of the ruleset is responsible for testing the actual rule logic.
25
+ *
26
+ * When custom rules are added:
27
+ * - They'll have dedicated test files for in-depth testing
28
+ * - They should also have a smoke test here to verify they're registered
29
+ */
30
+ describe('config', () => {
31
+ describe('custom no-restricted-globals rule', () => {
32
+ it('should error when using restricted globals', async () => {
33
+ const code = `
34
+ const path = location.pathname;
35
+ `;
36
+ const messages = await lintCode(code, 'test.js');
37
+ const customRuleError = messages.find((m) => m.ruleId === '@hubspot/ui-extensions/no-restricted-globals');
38
+ expect(customRuleError).toBeDefined();
39
+ expect(customRuleError?.severity).toBe(2);
40
+ expect(customRuleError?.message).toContain('location');
41
+ });
42
+ it('should allow local variables with restricted names', async () => {
43
+ const code = `
44
+ const location = { pathname: '/test' };
45
+ console.log(location.pathname);
46
+ `;
47
+ const messages = await lintCode(code, 'test.js');
48
+ const customRuleError = messages.find((m) => m.ruleId === '@hubspot/ui-extensions/no-restricted-globals');
49
+ expect(customRuleError).toBeUndefined();
50
+ });
51
+ });
52
+ describe('JSX support', () => {
53
+ it('should parse JSX syntax without errors', async () => {
54
+ const code = `
55
+ function App() {
56
+ return <Button>Click Me</Button>;
57
+ }
58
+ `;
59
+ const messages = await lintCode(code, 'test.tsx');
60
+ const parseErrors = messages.filter((m) => m.fatal);
61
+ expect(parseErrors).toHaveLength(0);
62
+ });
63
+ });
64
+ describe('custom no-html-elements rule', () => {
65
+ it('should error when using unsupported HTML elements', async () => {
66
+ const code = `
67
+ const element = <div>Hello</div>;
68
+ `;
69
+ const messages = await lintCode(code, 'test.jsx');
70
+ const customRuleError = messages.find((m) => m.ruleId === '@hubspot/ui-extensions/no-html-elements');
71
+ expect(customRuleError).toBeDefined();
72
+ expect(customRuleError?.severity).toBe(2);
73
+ });
74
+ it('should allow usage of supported elements', async () => {
75
+ const code = `
76
+ const element = <Button>Click Me</Button>;
77
+ `;
78
+ const messages = await lintCode(code, 'test.jsx');
79
+ const customRuleError = messages.find((m) => m.ruleId === '@hubspot/ui-extensions/no-html-elements');
80
+ expect(customRuleError).toBeUndefined();
81
+ });
82
+ });
83
+ describe('custom no-invalid-extension-point-imports rule', () => {
84
+ it('should error when importing from an invalid extension point', async () => {
85
+ const code = `
86
+ import { SettingsComponent } from '@hubspot/ui-extensions/settings';
87
+ `;
88
+ const messages = await lintCode(code, 'src/app/cards/MyCard.tsx', '/path/to/project/src/app/cards');
89
+ const customRuleError = messages.find((m) => m.ruleId ===
90
+ '@hubspot/ui-extensions/no-invalid-extension-point-imports');
91
+ expect(customRuleError).toBeDefined();
92
+ expect(customRuleError?.severity).toBe(2);
93
+ expect(customRuleError?.message).toContain('Imports from "settings" are not allowed in cards.');
94
+ });
95
+ it('should allow valid extension point imports', async () => {
96
+ const code = `
97
+ import { useCrmProperties } from '@hubspot/ui-extensions/crm/hooks';
98
+ `;
99
+ const messages = await lintCode(code, 'src/app/cards/MyCard.tsx', '/path/to/project/src/app/cards');
100
+ const customRuleError = messages.find((m) => m.ruleId ===
101
+ '@hubspot/ui-extensions/no-invalid-extension-point-imports');
102
+ expect(customRuleError).toBeUndefined();
103
+ });
104
+ });
105
+ describe('custom no-native-http rule', () => {
106
+ it('should error when using native fetch()', async () => {
107
+ const code = `
108
+ function getData() {
109
+ return fetch('https://api.example.com');
110
+ }
111
+ `;
112
+ const messages = await lintCode(code, 'test.js');
113
+ const customRuleError = messages.find((m) => m.ruleId === '@hubspot/ui-extensions/no-native-http');
114
+ expect(customRuleError).toBeDefined();
115
+ expect(customRuleError?.severity).toBe(2);
116
+ expect(customRuleError?.message).toContain('Native fetch()');
117
+ });
118
+ it('should allow hubspot.fetch()', async () => {
119
+ const code = `
120
+ function getData() {
121
+ return hubspot.fetch('https://api.example.com');
122
+ }
123
+ `;
124
+ const messages = await lintCode(code, 'test.js');
125
+ const customRuleError = messages.find((m) => m.ruleId === '@hubspot/ui-extensions/no-native-http');
126
+ expect(customRuleError).toBeUndefined();
127
+ });
128
+ });
129
+ describe('custom no-browser-dialogs rule', () => {
130
+ it('should error when using native alert()', async () => {
131
+ const code = `
132
+ function showMessage() {
133
+ alert('Hello World');
134
+ }
135
+ `;
136
+ const messages = await lintCode(code, 'test.js');
137
+ const customRuleError = messages.find((m) => m.ruleId === '@hubspot/ui-extensions/no-browser-dialogs');
138
+ expect(customRuleError).toBeDefined();
139
+ expect(customRuleError?.severity).toBe(2);
140
+ expect(customRuleError?.message).toContain('alert');
141
+ });
142
+ it('should allow custom alert functions', async () => {
143
+ const code = `
144
+ function showMessage() {
145
+ const alert = (msg) => actions.addAlert(msg);
146
+ alert('Hello World');
147
+ }
148
+ `;
149
+ const messages = await lintCode(code, 'test.js');
150
+ const customRuleError = messages.find((m) => m.ruleId === '@hubspot/ui-extensions/no-browser-dialogs');
151
+ expect(customRuleError).toBeUndefined();
152
+ });
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
+ });
174
+ describe('custom no-console rule', () => {
175
+ it('should warn when using console.log()', async () => {
176
+ const code = `
177
+ function debug() {
178
+ console.log('Debug message');
179
+ }
180
+ `;
181
+ const messages = await lintCode(code, 'test.js');
182
+ const customRuleWarning = messages.find((m) => m.ruleId === '@hubspot/ui-extensions/no-console');
183
+ expect(customRuleWarning).toBeDefined();
184
+ expect(customRuleWarning?.severity).toBe(1);
185
+ expect(customRuleWarning?.message).toContain('console.log()');
186
+ });
187
+ it('should allow custom log functions', async () => {
188
+ const code = `
189
+ function debug() {
190
+ const log = (msg) => logger.debug(msg);
191
+ log('Debug message');
192
+ }
193
+ `;
194
+ const messages = await lintCode(code, 'test.js');
195
+ const customRuleWarning = messages.find((m) => m.ruleId === '@hubspot/ui-extensions/no-console');
196
+ expect(customRuleWarning).toBeUndefined();
197
+ });
198
+ });
199
+ describe('custom no-dom-access rule', () => {
200
+ it('should error when accessing document', async () => {
201
+ const code = `
202
+ function getElement() {
203
+ return document.querySelector('.test');
204
+ }
205
+ `;
206
+ const messages = await lintCode(code, 'test.js');
207
+ const customRuleError = messages.find((m) => m.ruleId === '@hubspot/ui-extensions/no-dom-access');
208
+ expect(customRuleError).toBeDefined();
209
+ expect(customRuleError?.severity).toBe(2);
210
+ expect(customRuleError?.message).toContain('document');
211
+ });
212
+ it('should allow local document variable', async () => {
213
+ const code = `
214
+ function parse() {
215
+ const document = { body: null };
216
+ return document.body;
217
+ }
218
+ `;
219
+ const messages = await lintCode(code, 'test.js');
220
+ const customRuleError = messages.find((m) => m.ruleId === '@hubspot/ui-extensions/no-dom-access');
221
+ expect(customRuleError).toBeUndefined();
222
+ });
223
+ });
224
+ describe('custom no-parent-imports rule', () => {
225
+ it('should error when importing from outside extension point directory', async () => {
226
+ const code = `
227
+ import { util } from '../shared/utils';
228
+ `;
229
+ const messages = await lintCode(code, 'src/app/cards/Component.tsx', '/path/to/project');
230
+ const customRuleError = messages.find((m) => m.ruleId === '@hubspot/ui-extensions/no-parent-imports');
231
+ expect(customRuleError).toBeDefined();
232
+ expect(customRuleError?.severity).toBe(2);
233
+ expect(customRuleError?.message).toContain('Imports from outside "cards" will fail at build time');
234
+ expect(customRuleError?.message).toContain('npm workspaces');
235
+ });
236
+ it('should allow imports within extension point directory', async () => {
237
+ const code = `
238
+ import { util } from './utils';
239
+ import { Component } from '../components/Button';
240
+ `;
241
+ const messages = await lintCode(code, 'src/app/cards/nested/Component.tsx', '/path/to/project');
242
+ const customRuleError = messages.find((m) => m.ruleId === '@hubspot/ui-extensions/no-parent-imports');
243
+ expect(customRuleError).toBeUndefined();
244
+ });
245
+ it('should allow npm package imports', async () => {
246
+ const code = `
247
+ import { useState } from 'react';
248
+ import { Button } from '@hubspot/ui-extensions';
249
+ `;
250
+ const messages = await lintCode(code, 'src/app/cards/Component.tsx', '/path/to/project');
251
+ const customRuleError = messages.find((m) => m.ruleId === '@hubspot/ui-extensions/no-parent-imports');
252
+ expect(customRuleError).toBeUndefined();
253
+ });
254
+ });
255
+ describe('custom no-browser-storage rule', () => {
256
+ it('should error when accessing localStorage', async () => {
257
+ const code = `
258
+ function saveData() {
259
+ localStorage.setItem('key', 'value');
260
+ }
261
+ `;
262
+ const messages = await lintCode(code, 'test.js');
263
+ const customRuleError = messages.find((m) => m.ruleId === '@hubspot/ui-extensions/no-browser-storage');
264
+ expect(customRuleError).toBeDefined();
265
+ expect(customRuleError?.severity).toBe(2);
266
+ expect(customRuleError?.message).toContain('localStorage');
267
+ });
268
+ it('should error when accessing sessionStorage', async () => {
269
+ const code = `
270
+ function loadData() {
271
+ return sessionStorage.getItem('key');
272
+ }
273
+ `;
274
+ const messages = await lintCode(code, 'test.js');
275
+ const customRuleError = messages.find((m) => m.ruleId === '@hubspot/ui-extensions/no-browser-storage');
276
+ expect(customRuleError).toBeDefined();
277
+ expect(customRuleError?.severity).toBe(2);
278
+ expect(customRuleError?.message).toContain('sessionStorage');
279
+ });
280
+ it('should allow local localStorage variable', async () => {
281
+ const code = `
282
+ function saveData() {
283
+ const localStorage = { setItem: () => {} };
284
+ localStorage.setItem('key', 'value');
285
+ }
286
+ `;
287
+ const messages = await lintCode(code, 'test.js');
288
+ const customRuleError = messages.find((m) => m.ruleId === '@hubspot/ui-extensions/no-browser-storage');
289
+ expect(customRuleError).toBeUndefined();
290
+ });
291
+ it('should allow local sessionStorage variable', async () => {
292
+ const code = `
293
+ function loadData() {
294
+ const sessionStorage = { getItem: () => null };
295
+ return sessionStorage.getItem('key');
296
+ }
297
+ `;
298
+ const messages = await lintCode(code, 'test.js');
299
+ const customRuleError = messages.find((m) => m.ruleId === '@hubspot/ui-extensions/no-browser-storage');
300
+ expect(customRuleError).toBeUndefined();
301
+ });
302
+ });
303
+ });
@@ -0,0 +1 @@
1
+ export {};