@electerm/electerm-react 1.51.8 → 1.51.20

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 (56) hide show
  1. package/client/common/constants.js +16 -1
  2. package/client/common/default-setting.js +0 -1
  3. package/client/common/is-color-dark.js +15 -11
  4. package/client/common/new-terminal.js +2 -5
  5. package/client/common/reverse-color.js +12 -0
  6. package/client/common/ws.js +8 -1
  7. package/client/components/bookmark-form/render-connection-hopping.jsx +25 -2
  8. package/client/components/bookmark-form/ssh-form.jsx +1 -2
  9. package/client/components/bookmark-form/tree-delete.jsx +5 -10
  10. package/client/components/bookmark-form/use-ui.jsx +1 -2
  11. package/client/components/common/connection-hopping-warning-text.jsx +36 -0
  12. package/client/components/common/drag-handle.jsx +60 -0
  13. package/client/components/common/drag-handle.styl +12 -0
  14. package/client/components/layout/layout.jsx +3 -2
  15. package/client/components/main/connection-hopping-warnning.jsx +45 -0
  16. package/client/components/main/error-wrapper.jsx +120 -5
  17. package/client/components/main/main.jsx +23 -3
  18. package/client/components/main/upgrade.jsx +4 -9
  19. package/client/components/main/wrapper.styl +2 -1
  20. package/client/components/profile/profile-form-ssh.jsx +1 -1
  21. package/client/components/rdp/resolution-edit.jsx +3 -5
  22. package/client/components/session/session.jsx +7 -2
  23. package/client/components/setting-panel/setting-common.jsx +28 -7
  24. package/client/components/setting-panel/setting-terminal.jsx +7 -4
  25. package/client/components/sftp/confirm-modal-store.jsx +3 -4
  26. package/client/components/sftp/sftp-entry.jsx +2 -5
  27. package/client/components/sftp/transfer-conflict-store.jsx +1 -3
  28. package/client/components/sftp/transport-action-store.jsx +21 -10
  29. package/client/components/side-panel-r/side-panel-r.jsx +13 -40
  30. package/client/components/sidebar/bookmark-select.jsx +0 -3
  31. package/client/components/sidebar/index.jsx +1 -6
  32. package/client/components/sidebar/side-panel.jsx +27 -51
  33. package/client/components/sidebar/sidebar.styl +0 -9
  34. package/client/components/ssh-config/load-ssh-configs.jsx +106 -0
  35. package/client/components/ssh-config/ssh-config-item.jsx +26 -0
  36. package/client/components/ssh-config/ssh-config-load-notify.jsx +60 -0
  37. package/client/components/tabs/tab.jsx +9 -10
  38. package/client/components/tabs/tabs.styl +6 -1
  39. package/client/components/terminal/index.jsx +4 -18
  40. package/client/components/tree-list/bookmark-toolbar.jsx +203 -0
  41. package/client/components/tree-list/bookmark-transport.jsx +2 -0
  42. package/client/components/tree-list/tree-list.jsx +25 -32
  43. package/client/store/bookmark-group.js +2 -13
  44. package/client/store/bookmark.js +43 -1
  45. package/client/store/common.js +2 -8
  46. package/client/store/index.js +45 -50
  47. package/client/store/init-state.js +16 -20
  48. package/client/store/load-data.js +5 -10
  49. package/client/store/setting.js +8 -15
  50. package/client/store/sync.js +0 -1
  51. package/client/store/tab.js +22 -11
  52. package/client/store/terminal-theme.js +1 -1
  53. package/client/store/transfer-list.js +0 -8
  54. package/client/store/ui-theme.js +0 -9
  55. package/client/store/watch.js +8 -8
  56. package/package.json +1 -1
@@ -27,15 +27,16 @@ import batchInputHistory from './batch-input-history'
27
27
  import transferExtend from './transfer-list'
28
28
  import addressBookmarkExtend from './address-bookmark'
29
29
  import isColorDark from '../common/is-color-dark'
30
-
30
+ import { getReverseColor } from '../common/reverse-color'
31
31
  import { uniq } from 'lodash-es'
32
- import copy from 'json-deep-copy'
32
+ import deepCopy from 'json-deep-copy'
33
33
  import {
34
34
  settingMap,
35
35
  paneMap,
36
36
  settingSyncId,
37
37
  settingShortcutsId,
38
- settingTerminalId
38
+ settingTerminalId,
39
+ terminalSshConfigType
39
40
  } from '../common/constants'
40
41
  import getInitItem from '../common/init-setting-item'
41
42
  import {
@@ -44,19 +45,6 @@ import {
44
45
 
45
46
  const e = window.translate
46
47
 
47
- function getReverseColor (hex) {
48
- // Check if the input is a valid hex color code
49
- if (!/^#[0-9a-fA-F]{6}$/.test(hex)) {
50
- return '#0088cc'
51
- }
52
- // Convert the hex color code to an integer
53
- const num = parseInt(hex.slice(1), 16)
54
- // Bitwise XOR the integer with 0xFFFFFF to get the reverse color
55
- const reverse = num ^ 0xFFFFFF
56
- // Convert the reverse color to a hex string and pad with zeros if needed
57
- return '#' + reverse.toString(16).padStart(6, '0')
58
- }
59
-
60
48
  class Store {
61
49
  constructor () {
62
50
  Object.assign(
@@ -69,6 +57,10 @@ class Store {
69
57
  return window.store.innerWidth
70
58
  }
71
59
 
60
+ get config () {
61
+ return deepCopy(window.store._config)
62
+ }
63
+
72
64
  get currentQuickCommands () {
73
65
  const { currentTab } = this
74
66
  const { quickCommands } = window.store
@@ -100,12 +92,24 @@ class Store {
100
92
 
101
93
  get inActiveTerminal () {
102
94
  const { store } = window
103
- return !store.showModal &&
104
- store.currentTab &&
105
- (
106
- store.currentTab.pane === paneMap.ssh ||
107
- store.currentTab.pane === paneMap.terminal
108
- )
95
+ if (store.showModal) {
96
+ return false
97
+ }
98
+ const {
99
+ currentTab
100
+ } = store
101
+ if (!currentTab) {
102
+ return false
103
+ }
104
+ const {
105
+ type,
106
+ pane
107
+ } = currentTab
108
+ if (type === 'rdp' || type === 'vnc' || type === 'web') {
109
+ return false
110
+ }
111
+ return pane === paneMap.ssh ||
112
+ pane === paneMap.terminal
109
113
  }
110
114
 
111
115
  get quickCommandTags () {
@@ -124,6 +128,10 @@ class Store {
124
128
  return window.store.tabs.some(t => t.isTransporting)
125
129
  }
126
130
 
131
+ get transferCount () {
132
+ return window.store.fileTransfers.length
133
+ }
134
+
127
135
  get settingSidebarList () {
128
136
  const {
129
137
  settingTab
@@ -133,7 +141,7 @@ class Store {
133
141
  return settingTab === settingMap.history
134
142
  ? arr
135
143
  : [
136
- copy(initItem),
144
+ deepCopy(initItem),
137
145
  ...arr
138
146
  ]
139
147
  }
@@ -144,7 +152,7 @@ class Store {
144
152
  } = window
145
153
  const theme = store.getThemeConfig()
146
154
  return {
147
- ...JSON.parse(window.store._termSearchOptions),
155
+ ...window.store._termSearchOptions,
148
156
  decorations: {
149
157
  activeMatchBorder: theme.yellow,
150
158
  activeMatchBackground: theme.selectionBackground,
@@ -218,6 +226,12 @@ class Store {
218
226
  }, {})
219
227
  }
220
228
 
229
+ hasSshConfig () {
230
+ return !!window.store
231
+ .bookmarkGroups
232
+ .find(b => b.id === terminalSshConfigType)
233
+ }
234
+
221
235
  get bookmarkGroupTree () {
222
236
  const {
223
237
  bookmarkGroups
@@ -229,23 +243,22 @@ class Store {
229
243
  }
230
244
  }, {})
231
245
  }
246
+
247
+ get hasOldConnectionHoppingBookmark () {
248
+ return window.store.bookmarks.some(b => {
249
+ return b.connectionHoppings?.length && !b.hasHopping
250
+ })
251
+ }
232
252
  }
233
253
 
234
254
  const arrGetterProps = [
235
- 'expandedKeys',
236
- 'checkedKeys',
237
255
  'addressBookmarks',
238
256
  'addressBookmarksLocal',
239
- 'sshConfigItems',
240
- 'itermThemes',
241
257
  'bookmarks',
242
258
  'bookmarkGroups',
243
259
  'profiles',
244
260
  'quickCommands',
245
- 'terminalThemes',
246
- 'serials',
247
- 'fonts',
248
- 'resolutions'
261
+ 'terminalThemes'
249
262
  ]
250
263
 
251
264
  for (const prop of arrGetterProps) {
@@ -256,24 +269,6 @@ for (const prop of arrGetterProps) {
256
269
  })
257
270
  }
258
271
 
259
- const getterProps = [
260
- 'langs',
261
- 'config',
262
- 'sftpSortSetting',
263
- 'upgradeInfo',
264
- 'transferToConfirm',
265
- 'terminalInfoProps',
266
- 'settingItem'
267
- ]
268
-
269
- for (const prop of getterProps) {
270
- Object.defineProperty(Store.prototype, prop, {
271
- get: function () {
272
- return JSON.parse(window.store[`_${prop}`] || '{}')
273
- }
274
- })
275
- }
276
-
277
272
  loadDataExtend(Store)
278
273
  eventExtend(Store)
279
274
  dbUpgradeExtend(Store)
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * init static state
3
3
  */
4
-
5
4
  import {
6
5
  settingMap,
7
6
  defaultBookmarkGroupId,
@@ -24,6 +23,7 @@ import {
24
23
  } from '../common/constants'
25
24
  import { buildDefaultThemes } from '../common/terminal-theme'
26
25
  import * as ls from '../common/safe-local-storage'
26
+ import { exclude } from 'manate'
27
27
  import initSettingItem from '../common/init-setting-item'
28
28
 
29
29
  const e = window.translate
@@ -49,27 +49,28 @@ export default () => {
49
49
  tabs: [],
50
50
  activeTabId: '',
51
51
  history: ls.getItemJSON('history', []),
52
+ sshConfigs: [],
52
53
  _bookmarks: '[]',
53
54
  sidebarPanelTab: 'bookmarks',
54
55
  _profiles: '[]',
55
56
  _bookmarkGroups: JSON.stringify(
56
57
  getDefaultBookmarkGroups([])
57
58
  ),
58
- _config: '{}',
59
+ _config: {},
59
60
  _terminalThemes: JSON.stringify([
60
61
  buildDefaultThemes()
61
62
  ]),
62
- _itermThemes: '[]',
63
+ itermThemes: exclude([]),
63
64
  currentBookmarkGroupId: defaultBookmarkGroupId,
64
- _expandedKeys: ls.getItem(expandedKeysLsKey) || JSON.stringify([
65
+ expandedKeys: ls.getItemJSON(expandedKeysLsKey, [
65
66
  defaultBookmarkGroupId
66
67
  ]),
67
68
  bookmarkSelectMode: false,
68
- _checkedKeys: ls.getItem(checkedKeysLsKey) || '[]',
69
+ checkedKeys: ls.getItemJSON(checkedKeysLsKey, []),
69
70
  _addressBookmarks: '[]',
70
71
  _addressBookmarksLocal: ls.getItem(localAddrBookmarkLsKey) || '[]',
71
72
  openResolutionEdit: false,
72
- _resolutions: ls.getItem(resolutionsLsKey) || '[]',
73
+ resolutions: ls.getItemJSON(resolutionsLsKey, []),
73
74
 
74
75
  // init session control
75
76
  selectedSessions: [],
@@ -81,8 +82,8 @@ export default () => {
81
82
  transferTab: 'transfer',
82
83
  transferHistory: [],
83
84
  fileTransfers: [],
84
- _transferToConfirm: '{}',
85
- _sftpSortSetting: ls.getItem(sftpDefaultSortSettingKey) || JSON.stringify({
85
+ transferToConfirm: {},
86
+ sftpSortSetting: ls.getItemJSON(sftpDefaultSortSettingKey, {
86
87
  local: {
87
88
  prop: 'modifyTime',
88
89
  direction: 'asc'
@@ -100,14 +101,14 @@ export default () => {
100
101
  activeTabId1: '',
101
102
  activeTabId2: '',
102
103
  activeTabId3: '',
103
- _terminalInfoProps: '{}',
104
+ terminalInfoProps: {},
104
105
  rightPanelVisible: false,
105
106
  rightPanelPinned: false,
106
107
  rightPanelWidth: parseInt(ls.getItem(rightSidebarWidthKey), 10) || 500,
107
108
 
108
109
  // for settings related
109
110
  _setting: '',
110
- _settingItem: JSON.stringify(initSettingItem([], settingMap.bookmarks)),
111
+ settingItem: initSettingItem([], settingMap.bookmarks),
111
112
  settingTab: settingMap.bookmarks, // setting tab
112
113
  autofocustrigger: Date.now(),
113
114
  bookmarkId: undefined,
@@ -117,23 +118,21 @@ export default () => {
117
118
  isSyncingSetting: false,
118
119
  isSyncUpload: false,
119
120
  isSyncDownload: false,
120
- syncSetting: {},
121
121
  syncType: syncTypes.github,
122
- _fonts: '[]',
123
122
 
124
123
  // term search
125
124
  termSearchOpen: false,
126
125
  termSearch: '',
127
126
  termSearchMatchCount: 0,
128
127
  termSearchMatchIndex: 0,
129
- _termSearchOptions: JSON.stringify({
128
+ _termSearchOptions: {
130
129
  caseSensitive: false,
131
130
  wholeWord: false,
132
131
  regex: false,
133
132
  decorations: {
134
133
  activeMatchColorOverviewRuler: 'yellow'
135
134
  }
136
- }),
135
+ },
137
136
 
138
137
  // quick commands
139
138
  _quickCommands: '[]',
@@ -161,20 +160,17 @@ export default () => {
161
160
  showFileModal: false,
162
161
 
163
162
  // update
164
- _upgradeInfo: '{}',
163
+ upgradeInfo: {},
165
164
 
166
165
  // serial list related
167
- _serials: '[]',
166
+ serials: [],
168
167
  loaddingSerials: false,
169
168
 
170
- _sshConfigItems: '[]',
171
-
172
169
  appPath: '',
173
170
  exePath: '',
174
171
  isPortable: false,
175
172
  installSrc: '',
176
-
177
- _langs: '[]',
173
+ showSshConfigModal: false,
178
174
 
179
175
  // batch inputs
180
176
  batchInputs: ls.getItemJSON(batchInputLsKey, []),
@@ -152,7 +152,8 @@ export default (Store) => {
152
152
  console.log('fetchSshConfigItems error', err)
153
153
  return []
154
154
  })
155
- window.store._sshConfigItems = JSON.stringify(arr)
155
+ window.store.sshConfigs = arr
156
+ return arr
156
157
  }
157
158
  Store.prototype.confirmLoad = function () {
158
159
  window.store.configLoaded = true
@@ -165,12 +166,8 @@ export default (Store) => {
165
166
  store.appPath = globs.appPath
166
167
  store.exePath = globs.exePath
167
168
  store.isPortable = globs.isPortable
168
- store._config = JSON.stringify(
169
- globs.config
170
- )
171
- store._langs = JSON.stringify(
172
- globs.langs
173
- )
169
+ store._config = globs.config
170
+ window.et.langs = globs.langs
174
171
  store.zoom(store.config.zoom, false, true)
175
172
  await initWsCommon()
176
173
  }
@@ -198,9 +195,7 @@ export default (Store) => {
198
195
  store.loadFontList()
199
196
  store.fetchItermThemes()
200
197
  store.openInitSessions()
201
- if (!store.config.hideSshConfig) {
202
- store.fetchSshConfigItems()
203
- }
198
+ store.fetchSshConfigItems()
204
199
  store.initCommandLine().catch(store.onError)
205
200
  initWatch(store)
206
201
  if (store.config.checkUpdateOnStart) {
@@ -22,21 +22,15 @@ const e = window.translate
22
22
  export default Store => {
23
23
  Store.prototype.setConfig = function (conf) {
24
24
  const { store } = window
25
- store._config = JSON.stringify(
26
- {
27
- ...store.config,
28
- ...conf
29
- }
25
+ Object.assign(
26
+ store._config,
27
+ copy(conf)
30
28
  )
31
29
  }
32
30
  Store.prototype.setSftpSortSetting = function (conf) {
33
- const { store } = window
34
- const base = copy(store.sftpSortSetting)
35
- store._sftpSortSetting = JSON.stringify(
36
- {
37
- ...base,
38
- ...conf
39
- }
31
+ Object.assign(
32
+ window.store.sftpSortSetting,
33
+ conf
40
34
  )
41
35
  }
42
36
 
@@ -74,8 +68,7 @@ export default Store => {
74
68
  const { store } = window
75
69
  const bookmarks = store.bookmarks
76
70
  const item = copy(
77
- find(bookmarks, it => it.id === id) ||
78
- find(store.sshConfigItems, it => it.id === id)
71
+ find(bookmarks, it => it.id === id)
79
72
  )
80
73
  if (!item) {
81
74
  return
@@ -161,7 +154,7 @@ export default Store => {
161
154
  console.log('loadFontList error', err)
162
155
  return []
163
156
  })
164
- window.store._fonts = JSON.stringify(fonts)
157
+ window.et.fonts = fonts
165
158
  }
166
159
 
167
160
  Store.prototype.handleChangeSettingTab = function (settingTab) {
@@ -489,7 +489,6 @@ export default (Store) => {
489
489
  'hideIP',
490
490
  'terminalTimeout',
491
491
  'theme',
492
- 'terminalTypes',
493
492
  'language',
494
493
  'copyWhenSelect',
495
494
  'customCss',
@@ -13,10 +13,14 @@ import * as ls from '../common/safe-local-storage'
13
13
  import deepCopy from 'json-deep-copy'
14
14
  import generate from '../common/id-with-stamp'
15
15
  import uid from '../common/uid'
16
- import newTerm from '../common/new-terminal.js'
16
+ import newTerm, { updateCount } from '../common/new-terminal.js'
17
17
  import { action } from 'manate'
18
18
 
19
19
  export default Store => {
20
+ Store.prototype.nextTabCount = function () {
21
+ return updateCount()
22
+ }
23
+
20
24
  Store.prototype.getTabs = function () {
21
25
  return window.store.tabs
22
26
  }
@@ -36,7 +40,10 @@ export default Store => {
36
40
  store.fileTransfers.map(d => d.tabId)
37
41
  )
38
42
  store.tabs.forEach(tab => {
39
- tab.isTransporting = tabIdSet.has(tab.id)
43
+ const t = tabIdSet.has(tab.id)
44
+ if (Boolean(tab.isTransporting) !== t) {
45
+ tab.isTransporting = t
46
+ }
40
47
  })
41
48
  }
42
49
 
@@ -90,6 +97,7 @@ export default Store => {
90
97
  // Create copy of old tab with new ID
91
98
  const newTab = {
92
99
  ...oldTab,
100
+ tabCount: store.nextTabCount(),
93
101
  id: generate(), // Need to create new ID
94
102
  status: statusMap.processing // Reset status
95
103
  }
@@ -101,16 +109,17 @@ export default Store => {
101
109
  // Remove old tab
102
110
  tabs.splice(index, 1)
103
111
 
104
- // Update current tab ID if needed
105
- if (store.activeTabId === tabId) {
106
- store.activeTabId = newTab.id
107
- }
112
+ setTimeout(() => {
113
+ if (store.activeTabId === tabId) {
114
+ store.activeTabId = newTab.id
115
+ }
108
116
 
109
- // Update batch current tab ID if needed
110
- const batchProp = `activeTabId${oldBatch}`
111
- if (store[batchProp] === tabId) {
112
- store[batchProp] = newTab.id
113
- }
117
+ // Update batch current tab ID if needed
118
+ const batchProp = `activeTabId${oldBatch}`
119
+ if (store[batchProp] === tabId) {
120
+ store[batchProp] = newTab.id
121
+ }
122
+ }, 0)
114
123
  }
115
124
 
116
125
  Store.prototype.duplicateTab = function (tabId) {
@@ -127,6 +136,7 @@ export default Store => {
127
136
  const duplicatedTab = {
128
137
  ...deepCopy(sourceTab),
129
138
  id: generate(),
139
+ tabCount: store.nextTabCount(),
130
140
  status: statusMap.processing,
131
141
  isTransporting: undefined
132
142
  }
@@ -303,6 +313,7 @@ export default Store => {
303
313
  ) {
304
314
  const { store } = window
305
315
  const { tabs } = store
316
+ newTab.tabCount = store.nextTabCount()
306
317
  newTab.batch = batch ?? newTab.batch ?? window.openTabBatch ?? window.store.currentLayoutBatch
307
318
  if (typeof index === 'number' && index >= 0 && index <= tabs.length) {
308
319
  tabs.splice(index, 0, newTab)
@@ -65,7 +65,7 @@ export default Store => {
65
65
  }
66
66
 
67
67
  Store.prototype.setItermThemes = function (arr) {
68
- window.store._itermThemes = JSON.stringify(arr)
68
+ window.store.itermThemes = arr
69
69
  }
70
70
 
71
71
  Store.prototype.fetchItermThemes = async function () {
@@ -68,14 +68,6 @@ export default Store => {
68
68
  fileTransfers.splice(index, 1)
69
69
  }
70
70
 
71
- Store.prototype.removeTransfer = function (id) {
72
- const { fileTransfers } = window.store
73
- const index = fileTransfers.findIndex(t => t.id === id)
74
- if (index > -1) {
75
- fileTransfers.splice(index, 1)
76
- }
77
- }
78
-
79
71
  Store.prototype.skipAllTransfersSinceIndex = function (index) {
80
72
  window.store.fileTransfers.splice(index)
81
73
  }
@@ -15,15 +15,6 @@ import {
15
15
  import copy from 'json-deep-copy'
16
16
 
17
17
  export default Store => {
18
- // Store.prototype.toCss = async function (stylus) {
19
- // const { host, port } = window._config
20
- // const url = `http://${host}:${port}/to-css`
21
- // const data = await fetch.post(url, {
22
- // stylus
23
- // })
24
- // return data
25
- // }
26
-
27
18
  Store.prototype.getDefaultUiThemeConfig = function (stylus) {
28
19
  const reg = /[^\n]+ = [^\n]+\n/g
29
20
  const arr = stylus.match(reg)
@@ -47,8 +47,8 @@ export default store => {
47
47
  }
48
48
 
49
49
  autoRun(async () => {
50
- ls.setItem(resolutionsLsKey, store._resolutions)
51
- return store._resolutions
50
+ ls.setItemJSON(resolutionsLsKey, store.resolutions)
51
+ return store.resolutions
52
52
  }).start()
53
53
 
54
54
  autoRun(() => {
@@ -64,7 +64,7 @@ export default store => {
64
64
  if (!isEmpty(store.config)) {
65
65
  window.pre.runGlobalAsync('saveUserConfig', store.config)
66
66
  }
67
- return store._config
67
+ return store.config
68
68
  }, func => debounce(func, 100)).start()
69
69
 
70
70
  autoRun(() => {
@@ -74,17 +74,17 @@ export default store => {
74
74
 
75
75
  autoRun(() => {
76
76
  store.updateTabsStatus()
77
- return store.fileTransfers
78
- }, func => debounce(func, 100)).start()
77
+ return store.transferCount
78
+ }).start()
79
79
 
80
80
  autoRun(() => {
81
81
  ls.setItemJSON(sftpDefaultSortSettingKey, store.sftpSortSetting)
82
- return store._sftpSortSetting
82
+ return store.sftpSortSetting
83
83
  }).start()
84
84
 
85
85
  autoRun(() => {
86
86
  ls.setItemJSON(expandedKeysLsKey, store.expandedKeys)
87
- return store._expandedKeys
87
+ return store.expandedKeys
88
88
  }).start()
89
89
 
90
90
  autoRun(() => {
@@ -94,7 +94,7 @@ export default store => {
94
94
 
95
95
  autoRun(() => {
96
96
  ls.setItemJSON(checkedKeysLsKey, store.checkedKeys)
97
- return store._checkedKeys
97
+ return store.checkedKeys
98
98
  }).start()
99
99
 
100
100
  autoRun(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "1.51.8",
3
+ "version": "1.51.20",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",