jupyterlab-kernel-terminal-workspace-culler-extension 1.0.21__py3-none-any.whl

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 (23) hide show
  1. jupyterlab_kernel_terminal_workspace_culler_extension/__init__.py +54 -0
  2. jupyterlab_kernel_terminal_workspace_culler_extension/_version.py +4 -0
  3. jupyterlab_kernel_terminal_workspace_culler_extension/cli.py +430 -0
  4. jupyterlab_kernel_terminal_workspace_culler_extension/culler.py +499 -0
  5. jupyterlab_kernel_terminal_workspace_culler_extension/routes.py +176 -0
  6. jupyterlab_kernel_terminal_workspace_culler_extension/tests/__init__.py +1 -0
  7. jupyterlab_kernel_terminal_workspace_culler_extension/tests/test_culler.py +269 -0
  8. jupyterlab_kernel_terminal_workspace_culler_extension/tests/test_routes.py +17 -0
  9. jupyterlab_kernel_terminal_workspace_culler_extension-1.0.21.data/data/etc/jupyter/jupyter_server_config.d/jupyterlab_kernel_terminal_workspace_culler_extension.json +7 -0
  10. jupyterlab_kernel_terminal_workspace_culler_extension-1.0.21.data/data/share/jupyter/labextensions/jupyterlab_kernel_terminal_workspace_culler_extension/install.json +5 -0
  11. jupyterlab_kernel_terminal_workspace_culler_extension-1.0.21.data/data/share/jupyter/labextensions/jupyterlab_kernel_terminal_workspace_culler_extension/package.json +219 -0
  12. jupyterlab_kernel_terminal_workspace_culler_extension-1.0.21.data/data/share/jupyter/labextensions/jupyterlab_kernel_terminal_workspace_culler_extension/schemas/jupyterlab_kernel_terminal_workspace_culler_extension/package.json.orig +214 -0
  13. jupyterlab_kernel_terminal_workspace_culler_extension-1.0.21.data/data/share/jupyter/labextensions/jupyterlab_kernel_terminal_workspace_culler_extension/schemas/jupyterlab_kernel_terminal_workspace_culler_extension/plugin.json +64 -0
  14. jupyterlab_kernel_terminal_workspace_culler_extension-1.0.21.data/data/share/jupyter/labextensions/jupyterlab_kernel_terminal_workspace_culler_extension/static/728.b056947597422f9e496c.js +1 -0
  15. jupyterlab_kernel_terminal_workspace_culler_extension-1.0.21.data/data/share/jupyter/labextensions/jupyterlab_kernel_terminal_workspace_culler_extension/static/750.b2aa372edac477cffcb9.js +1 -0
  16. jupyterlab_kernel_terminal_workspace_culler_extension-1.0.21.data/data/share/jupyter/labextensions/jupyterlab_kernel_terminal_workspace_culler_extension/static/remoteEntry.61977aac2cfde9b88947.js +1 -0
  17. jupyterlab_kernel_terminal_workspace_culler_extension-1.0.21.data/data/share/jupyter/labextensions/jupyterlab_kernel_terminal_workspace_culler_extension/static/style.js +4 -0
  18. jupyterlab_kernel_terminal_workspace_culler_extension-1.0.21.data/data/share/jupyter/labextensions/jupyterlab_kernel_terminal_workspace_culler_extension/static/third-party-licenses.json +16 -0
  19. jupyterlab_kernel_terminal_workspace_culler_extension-1.0.21.dist-info/METADATA +208 -0
  20. jupyterlab_kernel_terminal_workspace_culler_extension-1.0.21.dist-info/RECORD +23 -0
  21. jupyterlab_kernel_terminal_workspace_culler_extension-1.0.21.dist-info/WHEEL +4 -0
  22. jupyterlab_kernel_terminal_workspace_culler_extension-1.0.21.dist-info/entry_points.txt +2 -0
  23. jupyterlab_kernel_terminal_workspace_culler_extension-1.0.21.dist-info/licenses/LICENSE +29 -0
@@ -0,0 +1,269 @@
1
+ """Unit tests for the resource culler."""
2
+
3
+ from datetime import datetime, timedelta, timezone
4
+ from unittest.mock import AsyncMock, MagicMock, patch
5
+
6
+ import pytest
7
+
8
+ from jupyterlab_kernel_terminal_workspace_culler_extension.culler import ResourceCuller
9
+
10
+
11
+ @pytest.fixture
12
+ def mock_server_app():
13
+ """Create a mock server app with kernel, terminal, and session managers."""
14
+ app = MagicMock()
15
+
16
+ # Mock kernel manager
17
+ app.kernel_manager = MagicMock()
18
+ app.kernel_manager.list_kernel_ids.return_value = []
19
+ app.kernel_manager.get_kernel.return_value = None
20
+ app.kernel_manager.shutdown_kernel = AsyncMock()
21
+
22
+ # Mock terminal manager
23
+ app.terminal_manager = MagicMock()
24
+ app.terminal_manager.list.return_value = []
25
+ app.terminal_manager.terminate = AsyncMock()
26
+
27
+ # Mock session manager
28
+ app.session_manager = MagicMock()
29
+ app.session_manager.list_sessions = AsyncMock(return_value=[])
30
+ app.session_manager.delete_session = AsyncMock()
31
+
32
+ return app
33
+
34
+
35
+ @pytest.fixture
36
+ def culler(mock_server_app):
37
+ """Create a ResourceCuller instance with mocked server app."""
38
+ return ResourceCuller(mock_server_app)
39
+
40
+
41
+ class TestDefaultSettings:
42
+ """Test default settings initialization."""
43
+
44
+ def test_default_kernel_settings(self, culler):
45
+ """Verify default kernel culling settings."""
46
+ settings = culler.get_settings()
47
+ assert settings["kernelCullEnabled"] is True
48
+ assert settings["kernelCullIdleTimeout"] == 60 # 1 hour
49
+
50
+ def test_default_terminal_settings(self, culler):
51
+ """Verify default terminal culling settings."""
52
+ settings = culler.get_settings()
53
+ assert settings["terminalCullEnabled"] is True
54
+ assert settings["terminalCullIdleTimeout"] == 60 # 1 hour
55
+
56
+ def test_default_session_settings(self, culler):
57
+ """Verify default session culling settings."""
58
+ settings = culler.get_settings()
59
+ assert settings["sessionCullEnabled"] is False
60
+ assert settings["sessionCullIdleTimeout"] == 10080 # 7 days
61
+
62
+ def test_default_check_interval(self, culler):
63
+ """Verify default check interval."""
64
+ settings = culler.get_settings()
65
+ assert settings["cullCheckInterval"] == 5
66
+
67
+
68
+ class TestUpdateSettings:
69
+ """Test settings update functionality."""
70
+
71
+ def test_update_kernel_settings(self, culler):
72
+ """Test updating kernel culling settings."""
73
+ culler.update_settings({
74
+ "kernelCullEnabled": False,
75
+ "kernelCullIdleTimeout": 120
76
+ })
77
+ settings = culler.get_settings()
78
+ assert settings["kernelCullEnabled"] is False
79
+ assert settings["kernelCullIdleTimeout"] == 120
80
+
81
+ def test_update_terminal_settings(self, culler):
82
+ """Test updating terminal culling settings."""
83
+ culler.update_settings({
84
+ "terminalCullEnabled": False,
85
+ "terminalCullIdleTimeout": 45
86
+ })
87
+ settings = culler.get_settings()
88
+ assert settings["terminalCullEnabled"] is False
89
+ assert settings["terminalCullIdleTimeout"] == 45
90
+
91
+ def test_update_session_settings(self, culler):
92
+ """Test updating session culling settings."""
93
+ culler.update_settings({
94
+ "sessionCullEnabled": True,
95
+ "sessionCullIdleTimeout": 60
96
+ })
97
+ settings = culler.get_settings()
98
+ assert settings["sessionCullEnabled"] is True
99
+ assert settings["sessionCullIdleTimeout"] == 60
100
+
101
+ def test_partial_update(self, culler):
102
+ """Test that partial updates don't affect other settings."""
103
+ culler.update_settings({"kernelCullEnabled": False})
104
+ settings = culler.get_settings()
105
+ assert settings["kernelCullEnabled"] is False
106
+ # Other settings should remain at defaults
107
+ assert settings["terminalCullEnabled"] is True
108
+ assert settings["sessionCullEnabled"] is False
109
+
110
+
111
+ class TestCullIdleKernel:
112
+ """Test kernel culling functionality."""
113
+
114
+ @pytest.mark.asyncio
115
+ async def test_cull_idle_kernel(self, culler, mock_server_app):
116
+ """Test that idle kernel exceeding timeout gets culled."""
117
+ kernel_id = "test-kernel-123"
118
+ idle_time = datetime.now(timezone.utc) - timedelta(minutes=120)
119
+
120
+ mock_kernel = MagicMock()
121
+ mock_kernel.execution_state = "idle"
122
+ mock_kernel.last_activity = idle_time
123
+
124
+ mock_server_app.kernel_manager.list_kernel_ids.return_value = [kernel_id]
125
+ mock_server_app.kernel_manager.get_kernel.return_value = mock_kernel
126
+
127
+ culled = await culler._cull_kernels()
128
+
129
+ assert kernel_id in culled
130
+ mock_server_app.kernel_manager.shutdown_kernel.assert_called_once_with(kernel_id)
131
+
132
+ @pytest.mark.asyncio
133
+ async def test_skip_busy_kernel(self, culler, mock_server_app):
134
+ """Test that busy kernels are not culled."""
135
+ kernel_id = "busy-kernel-123"
136
+ idle_time = datetime.now(timezone.utc) - timedelta(minutes=120)
137
+
138
+ mock_kernel = MagicMock()
139
+ mock_kernel.execution_state = "busy"
140
+ mock_kernel.last_activity = idle_time
141
+
142
+ mock_server_app.kernel_manager.list_kernel_ids.return_value = [kernel_id]
143
+ mock_server_app.kernel_manager.get_kernel.return_value = mock_kernel
144
+
145
+ culled = await culler._cull_kernels()
146
+
147
+ assert kernel_id not in culled
148
+ mock_server_app.kernel_manager.shutdown_kernel.assert_not_called()
149
+
150
+ @pytest.mark.asyncio
151
+ async def test_skip_active_kernel(self, culler, mock_server_app):
152
+ """Test that recently active kernels are not culled."""
153
+ kernel_id = "active-kernel-123"
154
+ # Last activity 5 minutes ago - below 60 minute threshold
155
+ recent_time = datetime.now(timezone.utc) - timedelta(minutes=5)
156
+
157
+ mock_kernel = MagicMock()
158
+ mock_kernel.execution_state = "idle"
159
+ mock_kernel.last_activity = recent_time
160
+
161
+ mock_server_app.kernel_manager.list_kernel_ids.return_value = [kernel_id]
162
+ mock_server_app.kernel_manager.get_kernel.return_value = mock_kernel
163
+
164
+ culled = await culler._cull_kernels()
165
+
166
+ assert kernel_id not in culled
167
+ mock_server_app.kernel_manager.shutdown_kernel.assert_not_called()
168
+
169
+
170
+ class TestCullIdleTerminal:
171
+ """Test terminal culling functionality."""
172
+
173
+ @pytest.mark.asyncio
174
+ async def test_cull_idle_terminal(self, culler, mock_server_app):
175
+ """Test that idle terminal exceeding timeout gets culled."""
176
+ terminal_name = "terminal-1"
177
+ idle_time = datetime.now(timezone.utc) - timedelta(minutes=60)
178
+
179
+ mock_server_app.terminal_manager.list.return_value = [
180
+ {"name": terminal_name, "last_activity": idle_time}
181
+ ]
182
+
183
+ culled = await culler._cull_terminals()
184
+
185
+ assert terminal_name in culled
186
+ mock_server_app.terminal_manager.terminate.assert_called_once_with(terminal_name)
187
+
188
+ @pytest.mark.asyncio
189
+ async def test_skip_active_terminal(self, culler, mock_server_app):
190
+ """Test that recently active terminals are not culled."""
191
+ terminal_name = "terminal-1"
192
+ # Last activity 5 minutes ago - below 30 minute threshold
193
+ recent_time = datetime.now(timezone.utc) - timedelta(minutes=5)
194
+
195
+ mock_server_app.terminal_manager.list.return_value = [
196
+ {"name": terminal_name, "last_activity": recent_time}
197
+ ]
198
+
199
+ culled = await culler._cull_terminals()
200
+
201
+ assert terminal_name not in culled
202
+ mock_server_app.terminal_manager.terminate.assert_not_called()
203
+
204
+ @pytest.mark.asyncio
205
+ async def test_cull_terminal_with_iso_string(self, culler, mock_server_app):
206
+ """Test culling terminal with ISO string timestamp."""
207
+ terminal_name = "terminal-1"
208
+ idle_time = (datetime.now(timezone.utc) - timedelta(minutes=60)).isoformat()
209
+
210
+ mock_server_app.terminal_manager.list.return_value = [
211
+ {"name": terminal_name, "last_activity": idle_time}
212
+ ]
213
+
214
+ culled = await culler._cull_terminals()
215
+
216
+ assert terminal_name in culled
217
+
218
+
219
+ class TestCullResult:
220
+ """Test cull result retrieval."""
221
+
222
+ def test_get_last_cull_result_empty(self, culler):
223
+ """Test that initial cull result is empty."""
224
+ result = culler.get_last_cull_result()
225
+ assert result["kernels_culled"] == []
226
+ assert result["terminals_culled"] == []
227
+ assert result["sessions_culled"] == []
228
+
229
+ def test_result_consumed_flag(self, culler):
230
+ """Test that result is marked as consumed after retrieval."""
231
+ # First call returns empty (result_consumed is True initially)
232
+ result1 = culler.get_last_cull_result()
233
+
234
+ # Simulate a culling that produced results
235
+ culler._last_cull_result = {
236
+ "kernels_culled": ["kernel-1"],
237
+ "terminals_culled": [],
238
+ "sessions_culled": [],
239
+ }
240
+ culler._result_consumed = False
241
+
242
+ # First retrieval should return the result
243
+ result2 = culler.get_last_cull_result()
244
+ assert result2["kernels_culled"] == ["kernel-1"]
245
+
246
+ # Second retrieval should return empty (consumed)
247
+ result3 = culler.get_last_cull_result()
248
+ assert result3["kernels_culled"] == []
249
+
250
+
251
+ class TestStatus:
252
+ """Test status retrieval."""
253
+
254
+ def test_status_not_running(self, culler):
255
+ """Test status when culler is not running."""
256
+ status = culler.get_status()
257
+ assert status["running"] is False
258
+
259
+ def test_status_running(self, culler):
260
+ """Test status when culler is running."""
261
+ with patch.object(culler, "_periodic_callback", MagicMock()):
262
+ status = culler.get_status()
263
+ assert status["running"] is True
264
+
265
+ def test_status_includes_settings(self, culler):
266
+ """Test that status includes current settings."""
267
+ status = culler.get_status()
268
+ assert "settings" in status
269
+ assert "kernelCullEnabled" in status["settings"]
@@ -0,0 +1,17 @@
1
+ import json
2
+
3
+
4
+ async def test_hello(jp_fetch):
5
+ # When
6
+ response = await jp_fetch("jupyterlab-kernel-terminal-workspace-culler-extension", "hello")
7
+
8
+ # Then
9
+ assert response.code == 200
10
+ payload = json.loads(response.body)
11
+ assert payload == {
12
+ "data": (
13
+ "Hello, world!"
14
+ " This is the '/jupyterlab-kernel-terminal-workspace-culler-extension/hello' endpoint."
15
+ " Try visiting me in your browser!"
16
+ ),
17
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "ServerApp": {
3
+ "jpserver_extensions": {
4
+ "jupyterlab_kernel_terminal_workspace_culler_extension": true
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "packageManager": "python",
3
+ "packageName": "jupyterlab_kernel_terminal_workspace_culler_extension",
4
+ "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyterlab_kernel_terminal_workspace_culler_extension"
5
+ }
@@ -0,0 +1,219 @@
1
+ {
2
+ "name": "jupyterlab_kernel_terminal_workspace_culler_extension",
3
+ "version": "1.0.21",
4
+ "description": "Jupyterlab extension to kill unused kernels, terminals and workspaces. User can configure the idle time (minutes) after which the resource will be released automatically. This helps with the locked memory, insane number of terminals opened etc.",
5
+ "keywords": [
6
+ "jupyter",
7
+ "jupyterlab",
8
+ "jupyterlab-extension"
9
+ ],
10
+ "homepage": "https://github.com/stellarshenson/jupyterlab_kernel_terminal_workspace_culler_extension",
11
+ "bugs": {
12
+ "url": "https://github.com/stellarshenson/jupyterlab_kernel_terminal_workspace_culler_extension/issues"
13
+ },
14
+ "license": "BSD-3-Clause",
15
+ "author": {
16
+ "name": "Stellars Henson",
17
+ "email": "konrad.jelen@gmail.com"
18
+ },
19
+ "files": [
20
+ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
21
+ "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
22
+ "src/**/*.{ts,tsx}",
23
+ "schema/*.json"
24
+ ],
25
+ "main": "lib/index.js",
26
+ "types": "lib/index.d.ts",
27
+ "style": "style/index.css",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/stellarshenson/jupyterlab_kernel_terminal_workspace_culler_extension.git"
31
+ },
32
+ "scripts": {
33
+ "build": "jlpm build:lib && jlpm build:labextension:dev",
34
+ "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
35
+ "build:labextension": "jupyter labextension build .",
36
+ "build:labextension:dev": "jupyter labextension build --development True .",
37
+ "build:lib": "tsc --sourceMap",
38
+ "build:lib:prod": "tsc",
39
+ "clean": "jlpm clean:lib",
40
+ "clean:lib": "rimraf lib tsconfig.tsbuildinfo",
41
+ "clean:lintcache": "rimraf .eslintcache .stylelintcache",
42
+ "clean:labextension": "rimraf jupyterlab_kernel_terminal_workspace_culler_extension/labextension jupyterlab_kernel_terminal_workspace_culler_extension/_version.py",
43
+ "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache",
44
+ "eslint": "jlpm eslint:check --fix",
45
+ "eslint:check": "eslint . --cache --ext .ts,.tsx",
46
+ "install:extension": "jlpm build",
47
+ "lint": "jlpm stylelint && jlpm prettier && jlpm eslint",
48
+ "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check",
49
+ "prettier": "jlpm prettier:base --write --list-different",
50
+ "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"",
51
+ "prettier:check": "jlpm prettier:base --check",
52
+ "stylelint": "jlpm stylelint:check --fix",
53
+ "stylelint:check": "stylelint --cache \"style/**/*.css\"",
54
+ "test": "jest --coverage",
55
+ "watch": "run-p watch:src watch:labextension",
56
+ "watch:src": "tsc -w --sourceMap",
57
+ "watch:labextension": "jupyter labextension watch ."
58
+ },
59
+ "dependencies": {
60
+ "@jupyterlab/application": "^4.0.0",
61
+ "@jupyterlab/coreutils": "^6.0.0",
62
+ "@jupyterlab/services": "^7.0.0",
63
+ "@jupyterlab/settingregistry": "^4.0.0",
64
+ "@jupyterlab/terminal": "^4.0.0"
65
+ },
66
+ "devDependencies": {
67
+ "@jupyterlab/builder": "^4.0.0",
68
+ "@jupyterlab/testutils": "^4.0.0",
69
+ "@types/jest": "^29.2.0",
70
+ "@types/json-schema": "^7.0.11",
71
+ "@types/react": "^18.0.26",
72
+ "@types/react-addons-linked-state-mixin": "^0.14.22",
73
+ "@typescript-eslint/eslint-plugin": "^6.1.0",
74
+ "@typescript-eslint/parser": "^6.1.0",
75
+ "css-loader": "^6.7.1",
76
+ "eslint": "^8.36.0",
77
+ "eslint-config-prettier": "^8.8.0",
78
+ "eslint-plugin-prettier": "^5.0.0",
79
+ "jest": "^29.2.0",
80
+ "mkdirp": "^1.0.3",
81
+ "npm-run-all2": "^7.0.1",
82
+ "prettier": "^3.0.0",
83
+ "rimraf": "^5.0.10",
84
+ "source-map-loader": "^1.0.2",
85
+ "style-loader": "^3.3.1",
86
+ "stylelint": "^15.10.1",
87
+ "stylelint-config-recommended": "^13.0.0",
88
+ "stylelint-config-standard": "^34.0.0",
89
+ "stylelint-csstree-validator": "^3.0.0",
90
+ "stylelint-prettier": "^4.0.0",
91
+ "typescript": "~5.5.4",
92
+ "yjs": "^13.5.0"
93
+ },
94
+ "resolutions": {
95
+ "lib0": "0.2.111"
96
+ },
97
+ "sideEffects": [
98
+ "style/*.css",
99
+ "style/index.js"
100
+ ],
101
+ "styleModule": "style/index.js",
102
+ "publishConfig": {
103
+ "access": "public"
104
+ },
105
+ "jupyterlab": {
106
+ "discovery": {
107
+ "server": {
108
+ "managers": [
109
+ "pip"
110
+ ],
111
+ "base": {
112
+ "name": "jupyterlab_kernel_terminal_workspace_culler_extension"
113
+ }
114
+ }
115
+ },
116
+ "extension": true,
117
+ "outputDir": "jupyterlab_kernel_terminal_workspace_culler_extension/labextension",
118
+ "schemaDir": "schema",
119
+ "_build": {
120
+ "load": "static/remoteEntry.61977aac2cfde9b88947.js",
121
+ "extension": "./extension",
122
+ "style": "./style"
123
+ }
124
+ },
125
+ "eslintIgnore": [
126
+ "node_modules",
127
+ "dist",
128
+ "coverage",
129
+ "**/*.d.ts",
130
+ "tests",
131
+ "**/__tests__",
132
+ "ui-tests"
133
+ ],
134
+ "eslintConfig": {
135
+ "extends": [
136
+ "eslint:recommended",
137
+ "plugin:@typescript-eslint/eslint-recommended",
138
+ "plugin:@typescript-eslint/recommended",
139
+ "plugin:prettier/recommended"
140
+ ],
141
+ "parser": "@typescript-eslint/parser",
142
+ "parserOptions": {
143
+ "project": "tsconfig.json",
144
+ "sourceType": "module"
145
+ },
146
+ "plugins": [
147
+ "@typescript-eslint"
148
+ ],
149
+ "rules": {
150
+ "@typescript-eslint/naming-convention": [
151
+ "error",
152
+ {
153
+ "selector": "interface",
154
+ "format": [
155
+ "PascalCase"
156
+ ],
157
+ "custom": {
158
+ "regex": "^I[A-Z]",
159
+ "match": true
160
+ }
161
+ }
162
+ ],
163
+ "@typescript-eslint/no-unused-vars": [
164
+ "warn",
165
+ {
166
+ "args": "none"
167
+ }
168
+ ],
169
+ "@typescript-eslint/no-explicit-any": "off",
170
+ "@typescript-eslint/no-namespace": "off",
171
+ "@typescript-eslint/no-use-before-define": "off",
172
+ "@typescript-eslint/quotes": [
173
+ "error",
174
+ "single",
175
+ {
176
+ "avoidEscape": true,
177
+ "allowTemplateLiterals": false
178
+ }
179
+ ],
180
+ "curly": [
181
+ "error",
182
+ "all"
183
+ ],
184
+ "eqeqeq": "error",
185
+ "prefer-arrow-callback": "error"
186
+ }
187
+ },
188
+ "prettier": {
189
+ "singleQuote": true,
190
+ "trailingComma": "none",
191
+ "arrowParens": "avoid",
192
+ "endOfLine": "auto",
193
+ "overrides": [
194
+ {
195
+ "files": "package.json",
196
+ "options": {
197
+ "tabWidth": 4
198
+ }
199
+ }
200
+ ]
201
+ },
202
+ "stylelint": {
203
+ "extends": [
204
+ "stylelint-config-recommended",
205
+ "stylelint-config-standard",
206
+ "stylelint-prettier/recommended"
207
+ ],
208
+ "plugins": [
209
+ "stylelint-csstree-validator"
210
+ ],
211
+ "rules": {
212
+ "csstree/validator": true,
213
+ "property-no-vendor-prefix": null,
214
+ "selector-class-pattern": "^([a-z][A-z\\d]*)(-[A-z\\d]+)*$",
215
+ "selector-no-vendor-prefix": null,
216
+ "value-no-vendor-prefix": null
217
+ }
218
+ }
219
+ }