@tanstack/devtools 0.6.19 → 0.6.21
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/dist/chunk/{XFQ6P775.js → XF4JFOLU.js} +15 -0
- package/dist/dev.js +13 -3
- package/dist/devtools/MBQPV7BO.js +3584 -0
- package/dist/devtools/{3YT62TLF.js → YRFZDV5N.js} +2334 -231
- package/dist/index.js +13 -3
- package/dist/server.js +9 -2
- package/package.json +5 -2
- package/src/components/source-inspector.tsx +158 -0
- package/src/context/devtools-context.tsx +24 -1
- package/src/core.tsx +15 -1
- package/src/devtools.tsx +3 -28
- package/src/styles/use-styles.ts +894 -53
- package/src/tabs/marketplace/card-utils.test.ts +219 -0
- package/src/tabs/marketplace/card-utils.ts +85 -0
- package/src/tabs/marketplace/marketplace-header.tsx +54 -0
- package/src/tabs/marketplace/plugin-card.tsx +165 -0
- package/src/tabs/marketplace/plugin-section.tsx +51 -0
- package/src/tabs/marketplace/plugin-utils.test.ts +518 -0
- package/src/tabs/marketplace/plugin-utils.ts +248 -0
- package/src/tabs/marketplace/settings-panel.tsx +41 -0
- package/src/tabs/marketplace/tag-filters.tsx +35 -0
- package/src/tabs/marketplace/types.ts +47 -0
- package/src/tabs/plugin-marketplace.tsx +346 -0
- package/src/tabs/plugin-registry.ts +222 -0
- package/src/tabs/plugins-tab.tsx +112 -65
- package/src/tabs/semver-utils.test.ts +218 -0
- package/src/tabs/semver-utils.ts +114 -0
- package/dist/devtools/MV3V7CMW.js +0 -1735
package/src/tabs/plugins-tab.tsx
CHANGED
|
@@ -1,21 +1,33 @@
|
|
|
1
|
-
import { For, createEffect, createMemo, createSignal } from 'solid-js'
|
|
1
|
+
import { For, Show, createEffect, createMemo, createSignal } from 'solid-js'
|
|
2
2
|
import clsx from 'clsx'
|
|
3
3
|
import { useDrawContext } from '../context/draw-context'
|
|
4
4
|
import { usePlugins, useTheme } from '../context/use-devtools-context'
|
|
5
5
|
import { useStyles } from '../styles/use-styles'
|
|
6
6
|
import { PLUGIN_CONTAINER_ID, PLUGIN_TITLE_CONTAINER_ID } from '../constants'
|
|
7
|
+
import { PluginMarketplace } from './plugin-marketplace'
|
|
7
8
|
|
|
8
9
|
export const PluginsTab = () => {
|
|
9
10
|
const { plugins, activePlugins, toggleActivePlugins } = usePlugins()
|
|
10
|
-
const { expanded, hoverUtils, animationMs } = useDrawContext()
|
|
11
|
+
const { expanded, hoverUtils, animationMs, setForceExpand } = useDrawContext()
|
|
11
12
|
|
|
12
13
|
const [pluginRefs, setPluginRefs] = createSignal(
|
|
13
14
|
new Map<string, HTMLDivElement>(),
|
|
14
15
|
)
|
|
16
|
+
const [showMarketplace, setShowMarketplace] = createSignal(false)
|
|
15
17
|
|
|
16
18
|
const styles = useStyles()
|
|
17
19
|
|
|
18
20
|
const { theme } = useTheme()
|
|
21
|
+
|
|
22
|
+
const hasPlugins = createMemo(
|
|
23
|
+
() => plugins()?.length && plugins()!.length > 0,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
// Keep sidebar expanded when marketplace is shown
|
|
27
|
+
createEffect(() => {
|
|
28
|
+
setForceExpand(showMarketplace())
|
|
29
|
+
})
|
|
30
|
+
|
|
19
31
|
createEffect(() => {
|
|
20
32
|
const currentActivePlugins = plugins()?.filter((plugin) =>
|
|
21
33
|
activePlugins().includes(plugin.id!),
|
|
@@ -30,76 +42,111 @@ export const PluginsTab = () => {
|
|
|
30
42
|
})
|
|
31
43
|
})
|
|
32
44
|
|
|
45
|
+
const handleMarketplaceClick = () => setShowMarketplace(!showMarketplace())
|
|
46
|
+
|
|
47
|
+
const handlePluginClick = (pluginId: string) => {
|
|
48
|
+
// Close marketplace when switching to a plugin
|
|
49
|
+
if (showMarketplace()) {
|
|
50
|
+
setShowMarketplace(false)
|
|
51
|
+
}
|
|
52
|
+
toggleActivePlugins(pluginId)
|
|
53
|
+
}
|
|
54
|
+
|
|
33
55
|
return (
|
|
34
|
-
<
|
|
35
|
-
<div
|
|
36
|
-
class={clsx(
|
|
37
|
-
styles().pluginsTabDraw(expanded()),
|
|
38
|
-
{
|
|
39
|
-
[styles().pluginsTabDraw(expanded())]: expanded(),
|
|
40
|
-
},
|
|
41
|
-
styles().pluginsTabDrawTransition(animationMs),
|
|
42
|
-
)}
|
|
43
|
-
onMouseEnter={() => hoverUtils.enter()}
|
|
44
|
-
onMouseLeave={() => hoverUtils.leave()}
|
|
45
|
-
>
|
|
56
|
+
<Show when={hasPlugins()} fallback={<PluginMarketplace />}>
|
|
57
|
+
<div class={styles().pluginsTabPanel}>
|
|
46
58
|
<div
|
|
47
59
|
class={clsx(
|
|
48
|
-
styles().
|
|
49
|
-
|
|
60
|
+
styles().pluginsTabDraw(expanded()),
|
|
61
|
+
{
|
|
62
|
+
[styles().pluginsTabDraw(expanded())]: expanded(),
|
|
63
|
+
},
|
|
64
|
+
styles().pluginsTabDrawTransition(animationMs),
|
|
50
65
|
)}
|
|
66
|
+
onMouseEnter={() => hoverUtils.enter()}
|
|
67
|
+
onMouseLeave={() => {
|
|
68
|
+
// Don't collapse on mouse leave if marketplace is open
|
|
69
|
+
if (!showMarketplace()) {
|
|
70
|
+
hoverUtils.leave()
|
|
71
|
+
}
|
|
72
|
+
}}
|
|
51
73
|
>
|
|
52
|
-
<
|
|
53
|
-
{(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
74
|
+
<div
|
|
75
|
+
class={clsx(
|
|
76
|
+
styles().pluginsTabSidebar(expanded()),
|
|
77
|
+
styles().pluginsTabSidebarTransition(animationMs),
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
<div class={styles().pluginsList}>
|
|
81
|
+
<For each={plugins()}>
|
|
82
|
+
{(plugin) => {
|
|
83
|
+
let pluginHeading: HTMLHeadingElement | undefined
|
|
84
|
+
|
|
85
|
+
createEffect(() => {
|
|
86
|
+
if (pluginHeading) {
|
|
87
|
+
typeof plugin.name === 'string'
|
|
88
|
+
? (pluginHeading.textContent = plugin.name)
|
|
89
|
+
: plugin.name(pluginHeading, theme())
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const isActive = createMemo(() =>
|
|
94
|
+
activePlugins().includes(plugin.id!),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div
|
|
99
|
+
onClick={() => handlePluginClick(plugin.id!)}
|
|
100
|
+
class={clsx(styles().pluginName, {
|
|
101
|
+
active: isActive(),
|
|
102
|
+
})}
|
|
103
|
+
>
|
|
104
|
+
<h3
|
|
105
|
+
id={`${PLUGIN_TITLE_CONTAINER_ID}-${plugin.id}`}
|
|
106
|
+
ref={pluginHeading}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
)
|
|
110
|
+
}}
|
|
111
|
+
</For>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{/* Add More Tab - visually distinct */}
|
|
115
|
+
<div
|
|
116
|
+
onClick={handleMarketplaceClick}
|
|
117
|
+
class={clsx(styles().pluginNameAddMore, {
|
|
118
|
+
active: showMarketplace(),
|
|
119
|
+
})}
|
|
120
|
+
>
|
|
121
|
+
<h3>Add More</h3>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{/* Show marketplace if active, otherwise show plugin panels */}
|
|
127
|
+
<Show
|
|
128
|
+
when={showMarketplace()}
|
|
129
|
+
fallback={
|
|
130
|
+
<For each={activePlugins()}>
|
|
131
|
+
{(pluginId) => (
|
|
69
132
|
<div
|
|
70
|
-
|
|
71
|
-
|
|
133
|
+
id={`${PLUGIN_CONTAINER_ID}-${pluginId}`}
|
|
134
|
+
ref={(el) => {
|
|
135
|
+
setPluginRefs((prev) => {
|
|
136
|
+
const updated = new Map(prev)
|
|
137
|
+
updated.set(pluginId, el)
|
|
138
|
+
return updated
|
|
139
|
+
})
|
|
72
140
|
}}
|
|
73
|
-
class={
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
</div>
|
|
82
|
-
)
|
|
83
|
-
}}
|
|
84
|
-
</For>
|
|
85
|
-
</div>
|
|
141
|
+
class={styles().pluginsTabContent}
|
|
142
|
+
/>
|
|
143
|
+
)}
|
|
144
|
+
</For>
|
|
145
|
+
}
|
|
146
|
+
>
|
|
147
|
+
<PluginMarketplace />
|
|
148
|
+
</Show>
|
|
86
149
|
</div>
|
|
87
|
-
|
|
88
|
-
<For each={activePlugins()}>
|
|
89
|
-
{(pluginId) => (
|
|
90
|
-
<div
|
|
91
|
-
id={`${PLUGIN_CONTAINER_ID}-${pluginId}`}
|
|
92
|
-
ref={(el) => {
|
|
93
|
-
setPluginRefs((prev) => {
|
|
94
|
-
const updated = new Map(prev)
|
|
95
|
-
updated.set(pluginId, el)
|
|
96
|
-
return updated
|
|
97
|
-
})
|
|
98
|
-
}}
|
|
99
|
-
class={styles().pluginsTabContent}
|
|
100
|
-
/>
|
|
101
|
-
)}
|
|
102
|
-
</For>
|
|
103
|
-
</div>
|
|
150
|
+
</Show>
|
|
104
151
|
)
|
|
105
152
|
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
compareVersions,
|
|
4
|
+
parseVersion,
|
|
5
|
+
satisfiesMaxVersion,
|
|
6
|
+
satisfiesMinVersion,
|
|
7
|
+
satisfiesVersionRange,
|
|
8
|
+
} from './semver-utils'
|
|
9
|
+
|
|
10
|
+
describe('parseVersion', () => {
|
|
11
|
+
it('should parse basic semver format', () => {
|
|
12
|
+
const result = parseVersion('1.2.3')
|
|
13
|
+
expect(result).toEqual({
|
|
14
|
+
major: 1,
|
|
15
|
+
minor: 2,
|
|
16
|
+
patch: 3,
|
|
17
|
+
raw: '1.2.3',
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('should parse version with v prefix', () => {
|
|
22
|
+
const result = parseVersion('v2.0.1')
|
|
23
|
+
expect(result).toEqual({
|
|
24
|
+
major: 2,
|
|
25
|
+
minor: 0,
|
|
26
|
+
patch: 1,
|
|
27
|
+
raw: 'v2.0.1',
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should parse version with ^ prefix', () => {
|
|
32
|
+
const result = parseVersion('^3.1.4')
|
|
33
|
+
expect(result).toEqual({
|
|
34
|
+
major: 3,
|
|
35
|
+
minor: 1,
|
|
36
|
+
patch: 4,
|
|
37
|
+
raw: '^3.1.4',
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should parse version with ~ prefix', () => {
|
|
42
|
+
const result = parseVersion('~1.5.2')
|
|
43
|
+
expect(result).toEqual({
|
|
44
|
+
major: 1,
|
|
45
|
+
minor: 5,
|
|
46
|
+
patch: 2,
|
|
47
|
+
raw: '~1.5.2',
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should handle prerelease versions', () => {
|
|
52
|
+
const result = parseVersion('1.0.0-alpha.1')
|
|
53
|
+
expect(result).toEqual({
|
|
54
|
+
major: 1,
|
|
55
|
+
minor: 0,
|
|
56
|
+
patch: 0,
|
|
57
|
+
raw: '1.0.0-alpha.1',
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('should handle build metadata', () => {
|
|
62
|
+
const result = parseVersion('1.0.0+20130313144700')
|
|
63
|
+
expect(result).toEqual({
|
|
64
|
+
major: 1,
|
|
65
|
+
minor: 0,
|
|
66
|
+
patch: 0,
|
|
67
|
+
raw: '1.0.0+20130313144700',
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should handle version with only major.minor', () => {
|
|
72
|
+
const result = parseVersion('2.1')
|
|
73
|
+
expect(result).toEqual({
|
|
74
|
+
major: 2,
|
|
75
|
+
minor: 1,
|
|
76
|
+
patch: 0,
|
|
77
|
+
raw: '2.1',
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should return null for invalid version', () => {
|
|
82
|
+
expect(parseVersion('')).toBeNull()
|
|
83
|
+
expect(parseVersion('invalid')).toBeNull()
|
|
84
|
+
expect(parseVersion('1')).toBeNull()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should return null for version with non-numeric parts', () => {
|
|
88
|
+
expect(parseVersion('a.b.c')).toBeNull()
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('compareVersions', () => {
|
|
93
|
+
it('should return 0 for equal versions', () => {
|
|
94
|
+
const v1 = parseVersion('1.2.3')!
|
|
95
|
+
const v2 = parseVersion('1.2.3')!
|
|
96
|
+
expect(compareVersions(v1, v2)).toBe(0)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('should return positive for newer major version', () => {
|
|
100
|
+
const v1 = parseVersion('2.0.0')!
|
|
101
|
+
const v2 = parseVersion('1.0.0')!
|
|
102
|
+
expect(compareVersions(v1, v2)).toBeGreaterThan(0)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should return negative for older major version', () => {
|
|
106
|
+
const v1 = parseVersion('1.0.0')!
|
|
107
|
+
const v2 = parseVersion('2.0.0')!
|
|
108
|
+
expect(compareVersions(v1, v2)).toBeLessThan(0)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should compare minor versions when major is equal', () => {
|
|
112
|
+
const v1 = parseVersion('1.5.0')!
|
|
113
|
+
const v2 = parseVersion('1.3.0')!
|
|
114
|
+
expect(compareVersions(v1, v2)).toBeGreaterThan(0)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('should compare patch versions when major and minor are equal', () => {
|
|
118
|
+
const v1 = parseVersion('1.2.5')!
|
|
119
|
+
const v2 = parseVersion('1.2.3')!
|
|
120
|
+
expect(compareVersions(v1, v2)).toBeGreaterThan(0)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe('satisfiesMinVersion', () => {
|
|
125
|
+
it('should return true when current version meets minimum', () => {
|
|
126
|
+
expect(satisfiesMinVersion('2.0.0', '1.0.0')).toBe(true)
|
|
127
|
+
expect(satisfiesMinVersion('1.5.0', '1.0.0')).toBe(true)
|
|
128
|
+
expect(satisfiesMinVersion('1.0.5', '1.0.0')).toBe(true)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('should return true when versions are equal', () => {
|
|
132
|
+
expect(satisfiesMinVersion('1.2.3', '1.2.3')).toBe(true)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should return false when current version is below minimum', () => {
|
|
136
|
+
expect(satisfiesMinVersion('0.9.0', '1.0.0')).toBe(false)
|
|
137
|
+
expect(satisfiesMinVersion('1.0.0', '1.5.0')).toBe(false)
|
|
138
|
+
expect(satisfiesMinVersion('1.2.3', '1.2.5')).toBe(false)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('should return true if version cannot be parsed', () => {
|
|
142
|
+
expect(satisfiesMinVersion('invalid', '1.0.0')).toBe(true)
|
|
143
|
+
expect(satisfiesMinVersion('1.0.0', 'invalid')).toBe(true)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('satisfiesMaxVersion', () => {
|
|
148
|
+
it('should return true when current version is below maximum', () => {
|
|
149
|
+
expect(satisfiesMaxVersion('1.0.0', '2.0.0')).toBe(true)
|
|
150
|
+
expect(satisfiesMaxVersion('1.5.0', '2.0.0')).toBe(true)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('should return true when versions are equal', () => {
|
|
154
|
+
expect(satisfiesMaxVersion('1.2.3', '1.2.3')).toBe(true)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('should return false when current version exceeds maximum', () => {
|
|
158
|
+
expect(satisfiesMaxVersion('2.0.0', '1.0.0')).toBe(false)
|
|
159
|
+
expect(satisfiesMaxVersion('1.5.0', '1.0.0')).toBe(false)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('should return true if version cannot be parsed', () => {
|
|
163
|
+
expect(satisfiesMaxVersion('invalid', '1.0.0')).toBe(true)
|
|
164
|
+
expect(satisfiesMaxVersion('1.0.0', 'invalid')).toBe(true)
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
describe('satisfiesVersionRange', () => {
|
|
169
|
+
it('should return satisfied when no range specified', () => {
|
|
170
|
+
expect(satisfiesVersionRange('1.0.0')).toEqual({ satisfied: true })
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('should validate minimum version only', () => {
|
|
174
|
+
expect(satisfiesVersionRange('2.0.0', '1.0.0')).toEqual({
|
|
175
|
+
satisfied: true,
|
|
176
|
+
})
|
|
177
|
+
expect(satisfiesVersionRange('0.9.0', '1.0.0')).toEqual({
|
|
178
|
+
satisfied: false,
|
|
179
|
+
reason: 'Requires v1.0.0 or higher (current: v0.9.0)',
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('should validate maximum version only', () => {
|
|
184
|
+
expect(satisfiesVersionRange('1.0.0', undefined, '2.0.0')).toEqual({
|
|
185
|
+
satisfied: true,
|
|
186
|
+
})
|
|
187
|
+
expect(satisfiesVersionRange('3.0.0', undefined, '2.0.0')).toEqual({
|
|
188
|
+
satisfied: false,
|
|
189
|
+
reason: 'Requires v2.0.0 or lower (current: v3.0.0)',
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('should validate both min and max versions', () => {
|
|
194
|
+
expect(satisfiesVersionRange('1.5.0', '1.0.0', '2.0.0')).toEqual({
|
|
195
|
+
satisfied: true,
|
|
196
|
+
})
|
|
197
|
+
expect(satisfiesVersionRange('0.9.0', '1.0.0', '2.0.0')).toEqual({
|
|
198
|
+
satisfied: false,
|
|
199
|
+
reason: 'Requires v1.0.0 or higher (current: v0.9.0)',
|
|
200
|
+
})
|
|
201
|
+
expect(satisfiesVersionRange('2.5.0', '1.0.0', '2.0.0')).toEqual({
|
|
202
|
+
satisfied: false,
|
|
203
|
+
reason: 'Requires v2.0.0 or lower (current: v2.5.0)',
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('should handle version prefixes', () => {
|
|
208
|
+
expect(satisfiesVersionRange('^1.5.0', '1.0.0', '2.0.0')).toEqual({
|
|
209
|
+
satisfied: true,
|
|
210
|
+
})
|
|
211
|
+
expect(satisfiesVersionRange('~1.5.0', '1.0.0', '2.0.0')).toEqual({
|
|
212
|
+
satisfied: true,
|
|
213
|
+
})
|
|
214
|
+
expect(satisfiesVersionRange('v1.5.0', '1.0.0', '2.0.0')).toEqual({
|
|
215
|
+
satisfied: true,
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
})
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple semver utilities for version comparison
|
|
3
|
+
* Handles basic semver format: major.minor.patch
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
interface ParsedVersion {
|
|
7
|
+
major: number
|
|
8
|
+
minor: number
|
|
9
|
+
patch: number
|
|
10
|
+
raw: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse a semver string into components
|
|
15
|
+
*/
|
|
16
|
+
export function parseVersion(version: string): ParsedVersion | null {
|
|
17
|
+
if (!version) return null
|
|
18
|
+
|
|
19
|
+
// Remove leading 'v', '^', '~', and any prerelease/build metadata
|
|
20
|
+
const cleanVersion = version
|
|
21
|
+
.replace(/^[v^~]/, '')
|
|
22
|
+
.split('-')[0]
|
|
23
|
+
?.split('+')[0]
|
|
24
|
+
|
|
25
|
+
if (!cleanVersion) return null
|
|
26
|
+
|
|
27
|
+
const parts = cleanVersion.split('.')
|
|
28
|
+
|
|
29
|
+
if (parts.length < 2) return null
|
|
30
|
+
|
|
31
|
+
const major = parseInt(parts[0] ?? '0', 10)
|
|
32
|
+
const minor = parseInt(parts[1] ?? '0', 10)
|
|
33
|
+
const patch = parseInt(parts[2] ?? '0', 10)
|
|
34
|
+
|
|
35
|
+
if (isNaN(major) || isNaN(minor) || isNaN(patch)) {
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
major,
|
|
41
|
+
minor,
|
|
42
|
+
patch,
|
|
43
|
+
raw: version,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Compare two versions
|
|
49
|
+
* Returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2
|
|
50
|
+
*/
|
|
51
|
+
export function compareVersions(v1: ParsedVersion, v2: ParsedVersion): number {
|
|
52
|
+
if (v1.major !== v2.major) return v1.major - v2.major
|
|
53
|
+
if (v1.minor !== v2.minor) return v1.minor - v2.minor
|
|
54
|
+
return v1.patch - v2.patch
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if a version satisfies a minimum requirement
|
|
59
|
+
*/
|
|
60
|
+
export function satisfiesMinVersion(
|
|
61
|
+
currentVersion: string,
|
|
62
|
+
minVersion: string,
|
|
63
|
+
): boolean {
|
|
64
|
+
const current = parseVersion(currentVersion)
|
|
65
|
+
const min = parseVersion(minVersion)
|
|
66
|
+
|
|
67
|
+
if (!current || !min) return true // If we can't parse, assume it's OK
|
|
68
|
+
|
|
69
|
+
return compareVersions(current, min) >= 0
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if a version is below a maximum requirement
|
|
74
|
+
*/
|
|
75
|
+
export function satisfiesMaxVersion(
|
|
76
|
+
currentVersion: string,
|
|
77
|
+
maxVersion: string,
|
|
78
|
+
): boolean {
|
|
79
|
+
const current = parseVersion(currentVersion)
|
|
80
|
+
const max = parseVersion(maxVersion)
|
|
81
|
+
|
|
82
|
+
if (!current || !max) return true
|
|
83
|
+
|
|
84
|
+
return compareVersions(current, max) <= 0
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check if a version satisfies both min and max requirements
|
|
89
|
+
*/
|
|
90
|
+
export function satisfiesVersionRange(
|
|
91
|
+
currentVersion: string,
|
|
92
|
+
minVersion?: string,
|
|
93
|
+
maxVersion?: string,
|
|
94
|
+
): { satisfied: boolean; reason?: string } {
|
|
95
|
+
if (!minVersion && !maxVersion) {
|
|
96
|
+
return { satisfied: true }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (minVersion && !satisfiesMinVersion(currentVersion, minVersion)) {
|
|
100
|
+
return {
|
|
101
|
+
satisfied: false,
|
|
102
|
+
reason: `Requires v${minVersion} or higher (current: v${currentVersion})`,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (maxVersion && !satisfiesMaxVersion(currentVersion, maxVersion)) {
|
|
107
|
+
return {
|
|
108
|
+
satisfied: false,
|
|
109
|
+
reason: `Requires v${maxVersion} or lower (current: v${currentVersion})`,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { satisfied: true }
|
|
114
|
+
}
|