capacitor-ota 1.0.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 +290 -0
- package/dist/index.d.mts +86 -0
- package/dist/index.mjs +582 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# OTA Service
|
|
2
|
+
|
|
3
|
+
Capacitor uygulamaları için Over-The-Air (OTA) güncelleme servisi.
|
|
4
|
+
|
|
5
|
+
## Mimari
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
9
|
+
│ ADMIN (Nadir) │
|
|
10
|
+
├──────────────────────────────────────────────────────────────┤
|
|
11
|
+
│ CLI/Panel → POST /api/versions/upload (Workers) │
|
|
12
|
+
│ ↓ │
|
|
13
|
+
│ 1. Bundle → R2: ota.silgi.dev/app/production/1.0.0.zip │
|
|
14
|
+
│ 2. JSON → R2: ota.silgi.dev/updates/com.example.app.json │
|
|
15
|
+
└──────────────────────────────────────────────────────────────┘
|
|
16
|
+
|
|
17
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
18
|
+
│ CİHAZ (Sık) │
|
|
19
|
+
├──────────────────────────────────────────────────────────────┤
|
|
20
|
+
│ App açılış → GET ota.silgi.dev/updates/com.example.app.json│
|
|
21
|
+
│ ↓ (CDN HIT - Workers YOK) │
|
|
22
|
+
│ Güncelleme var mı? │
|
|
23
|
+
│ ↓ evet │
|
|
24
|
+
│ GET ota.silgi.dev/bundle.zip (CDN HIT - Workers YOK) │
|
|
25
|
+
│ ↓ │
|
|
26
|
+
│ CapacitorUpdater.download() → set() │
|
|
27
|
+
└──────────────────────────────────────────────────────────────┘
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Sonuç:** Cihazlar için 0 Workers request, sadece R2 + CDN.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## CLI Kullanımı
|
|
35
|
+
|
|
36
|
+
### Kurulum
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
cd ota-service
|
|
40
|
+
pnpm install
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Komutlar
|
|
44
|
+
|
|
45
|
+
#### Login
|
|
46
|
+
```bash
|
|
47
|
+
pnpm cli login <username> <password>
|
|
48
|
+
```
|
|
49
|
+
Token `~/.ota-cli.json` dosyasına kaydedilir.
|
|
50
|
+
|
|
51
|
+
#### Uygulamaları Listele
|
|
52
|
+
```bash
|
|
53
|
+
pnpm cli apps
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Çıktı:
|
|
57
|
+
```
|
|
58
|
+
📱 Apps:
|
|
59
|
+
|
|
60
|
+
ID Name Created
|
|
61
|
+
──────────────────────────────────────────────────
|
|
62
|
+
9fa3a103d8d0 MyApp 1/22/2026
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
#### Versiyonları Listele
|
|
66
|
+
```bash
|
|
67
|
+
pnpm cli versions <app_id>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Çıktı:
|
|
71
|
+
```
|
|
72
|
+
📦 Versions:
|
|
73
|
+
|
|
74
|
+
ID Version Channel Active Downloads Created
|
|
75
|
+
─────────────────────────────────────────────────────────────────
|
|
76
|
+
1 1.0.0 production ✅ 150 1/20/2026
|
|
77
|
+
2 1.0.1 production 45 1/22/2026
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
#### Yeni Versiyon Yükle
|
|
81
|
+
```bash
|
|
82
|
+
pnpm cli upload <app_id> <path> [options]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`<path>` bir dizin veya `.zip` dosyası olabilir. Dizin verilirse CLI otomatik zipleyecektir.
|
|
86
|
+
|
|
87
|
+
**Seçenekler:**
|
|
88
|
+
| Flag | Kısa | Açıklama | Varsayılan |
|
|
89
|
+
|------|------|----------|------------|
|
|
90
|
+
| `--version` | `-v` | Versiyon numarası | 1.0.0 |
|
|
91
|
+
| `--channel` | `-c` | Kanal | production |
|
|
92
|
+
| `--capacitor-app-id` | | Capacitor app ID (static JSON için) | - |
|
|
93
|
+
| `--min-native` | | Minimum native versiyon | - |
|
|
94
|
+
| `--notes` | | Release notes | - |
|
|
95
|
+
|
|
96
|
+
**Örnekler:**
|
|
97
|
+
```bash
|
|
98
|
+
# Dizin ile (otomatik ziplenir)
|
|
99
|
+
pnpm cli upload 9fa3a103d8d0 ./dist \
|
|
100
|
+
-v 1.0.5 \
|
|
101
|
+
--capacitor-app-id com.example.app
|
|
102
|
+
|
|
103
|
+
# Zip dosyası ile
|
|
104
|
+
pnpm cli upload 9fa3a103d8d0 ./bundle.zip -v 1.0.5
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Çıktı:
|
|
108
|
+
```
|
|
109
|
+
📦 Zipping directory: dist...
|
|
110
|
+
📤 Uploading...
|
|
111
|
+
|
|
112
|
+
✅ Upload successful!
|
|
113
|
+
Version: 1.0.5
|
|
114
|
+
Checksum: a1b2c3d4...
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
#### Versiyon Sil
|
|
118
|
+
```bash
|
|
119
|
+
pnpm cli delete <version_id>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### Rollback
|
|
123
|
+
```bash
|
|
124
|
+
pnpm cli rollback <version_id>
|
|
125
|
+
```
|
|
126
|
+
Belirtilen versiyonu aktif yapar.
|
|
127
|
+
|
|
128
|
+
#### Konfigürasyon
|
|
129
|
+
```bash
|
|
130
|
+
# Mevcut ayarları göster
|
|
131
|
+
pnpm cli config
|
|
132
|
+
|
|
133
|
+
# API URL değiştir
|
|
134
|
+
pnpm cli set-url https://my-ota.workers.dev
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## API Endpoints
|
|
140
|
+
|
|
141
|
+
### Public (Auth Gerektirmez)
|
|
142
|
+
|
|
143
|
+
| Method | Endpoint | Açıklama |
|
|
144
|
+
|--------|----------|----------|
|
|
145
|
+
| GET | `/api/updates` | Update check (legacy) |
|
|
146
|
+
|
|
147
|
+
### Protected (Auth Gerektirir)
|
|
148
|
+
|
|
149
|
+
| Method | Endpoint | Açıklama |
|
|
150
|
+
|--------|----------|----------|
|
|
151
|
+
| POST | `/api/auth/login` | Login |
|
|
152
|
+
| GET | `/api/apps` | App listesi |
|
|
153
|
+
| POST | `/api/apps` | App oluştur |
|
|
154
|
+
| DELETE | `/api/apps/:id` | App sil |
|
|
155
|
+
| GET | `/api/versions` | Versiyon listesi |
|
|
156
|
+
| POST | `/api/versions/upload` | Versiyon yükle |
|
|
157
|
+
| DELETE | `/api/versions/:id` | Versiyon sil |
|
|
158
|
+
| POST | `/api/versions/:id/activate` | Versiyonu aktif yap |
|
|
159
|
+
| POST | `/api/versions/:id/rollback` | Rollback |
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Capacitor Entegrasyonu
|
|
164
|
+
|
|
165
|
+
### 1. Plugin Kur
|
|
166
|
+
```bash
|
|
167
|
+
npm install @capgo/capacitor-updater
|
|
168
|
+
npx cap sync
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### 2. Capacitor Config
|
|
172
|
+
```typescript
|
|
173
|
+
// capacitor.config.ts
|
|
174
|
+
const config: CapacitorConfig = {
|
|
175
|
+
plugins: {
|
|
176
|
+
CapacitorUpdater: {
|
|
177
|
+
autoUpdate: false, // Manuel kontrol
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### 3. Vue Composable
|
|
184
|
+
```typescript
|
|
185
|
+
// src/composables/useOtaUpdate.ts
|
|
186
|
+
import { CapacitorUpdater } from '@capgo/capacitor-updater'
|
|
187
|
+
|
|
188
|
+
const CONFIG = {
|
|
189
|
+
staticUrl: 'https://ota.silgi.dev',
|
|
190
|
+
appId: 'com.example.app',
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function useOtaUpdate() {
|
|
194
|
+
async function checkForUpdate() {
|
|
195
|
+
const url = `${CONFIG.staticUrl}/updates/${CONFIG.appId}.json`
|
|
196
|
+
const response = await fetch(url)
|
|
197
|
+
const data = await response.json()
|
|
198
|
+
// ... version karşılaştırma ve indirme
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function downloadUpdate(url: string, version: string, checksum: string) {
|
|
202
|
+
const bundle = await CapacitorUpdater.download({ url, version, checksum })
|
|
203
|
+
return bundle
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function applyUpdate(bundleId: string) {
|
|
207
|
+
await CapacitorUpdater.set({ id: bundleId })
|
|
208
|
+
// Otomatik reload
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { checkForUpdate, downloadUpdate, applyUpdate }
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### 4. App.vue
|
|
216
|
+
```vue
|
|
217
|
+
<script setup>
|
|
218
|
+
import { onMounted } from 'vue'
|
|
219
|
+
import { useOtaUpdate } from '@/composables/useOtaUpdate'
|
|
220
|
+
|
|
221
|
+
const ota = useOtaUpdate()
|
|
222
|
+
|
|
223
|
+
onMounted(async () => {
|
|
224
|
+
await ota.notifyAppReady() // Rollback'i önle
|
|
225
|
+
await ota.checkForUpdate()
|
|
226
|
+
})
|
|
227
|
+
</script>
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Static JSON Format
|
|
233
|
+
|
|
234
|
+
R2'de saklanan güncelleme bilgisi:
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
GET https://ota.silgi.dev/updates/com.example.app.json
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
```json
|
|
241
|
+
{
|
|
242
|
+
"version": "1.0.5",
|
|
243
|
+
"url": "https://ota.silgi.dev/9fa3a103d8d0/production/1.0.5.zip",
|
|
244
|
+
"checksum": "a1b2c3d4e5f6...",
|
|
245
|
+
"min_native_version": "1.0.0",
|
|
246
|
+
"release_notes": "Bug fixes",
|
|
247
|
+
"updated_at": "2026-01-22T10:30:00.000Z"
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Deployment
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
# Local development
|
|
257
|
+
pnpm dev
|
|
258
|
+
|
|
259
|
+
# Deploy to Cloudflare Workers
|
|
260
|
+
pnpm build
|
|
261
|
+
npx wrangler deploy
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Environment Variables
|
|
267
|
+
|
|
268
|
+
### Workers (wrangler.toml)
|
|
269
|
+
```toml
|
|
270
|
+
[vars]
|
|
271
|
+
ADMIN_PASSWORD_HASH = "bcrypt_hash_here"
|
|
272
|
+
R2_PUBLIC_URL = "https://ota.silgi.dev"
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Client (.env)
|
|
276
|
+
```env
|
|
277
|
+
VITE_OTA_STATIC_URL=https://ota.silgi.dev
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Maliyet Avantajı
|
|
283
|
+
|
|
284
|
+
| Senaryo | Workers Request | R2 Request |
|
|
285
|
+
|---------|-----------------|------------|
|
|
286
|
+
| 1000 kullanıcı, 10 açılış/gün | 0 | 10,000 |
|
|
287
|
+
| Admin upload (ayda ~10) | 10 | 10 |
|
|
288
|
+
|
|
289
|
+
**Workers:** Neredeyse ücretsiz (sadece admin)
|
|
290
|
+
**R2:** İlk 10M request/ay ücretsiz
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
interface OtaConfig {
|
|
3
|
+
staticUrl: string;
|
|
4
|
+
appId: string;
|
|
5
|
+
appVersion: string;
|
|
6
|
+
theme?: OtaTheme;
|
|
7
|
+
texts?: OtaTexts;
|
|
8
|
+
autoApply?: boolean;
|
|
9
|
+
showUI?: boolean;
|
|
10
|
+
}
|
|
11
|
+
interface OtaTheme {
|
|
12
|
+
primaryColor?: string;
|
|
13
|
+
gradientFrom?: string;
|
|
14
|
+
gradientTo?: string;
|
|
15
|
+
textColor?: string;
|
|
16
|
+
}
|
|
17
|
+
interface OtaTexts {
|
|
18
|
+
title?: string;
|
|
19
|
+
downloading?: string;
|
|
20
|
+
downloadComplete?: string;
|
|
21
|
+
applyButton?: string;
|
|
22
|
+
applying?: string;
|
|
23
|
+
errorTitle?: string;
|
|
24
|
+
retryButton?: string;
|
|
25
|
+
}
|
|
26
|
+
interface RemoteVersion {
|
|
27
|
+
version: string;
|
|
28
|
+
url: string;
|
|
29
|
+
checksum: string;
|
|
30
|
+
min_native_version?: string;
|
|
31
|
+
release_notes?: string | null;
|
|
32
|
+
}
|
|
33
|
+
type OtaStatus = 'idle' | 'checking' | 'downloading' | 'ready' | 'applying' | 'error';
|
|
34
|
+
interface OtaState {
|
|
35
|
+
status: OtaStatus;
|
|
36
|
+
progress: number;
|
|
37
|
+
currentVersion: string;
|
|
38
|
+
newVersion: string | null;
|
|
39
|
+
error: string | null;
|
|
40
|
+
}
|
|
41
|
+
interface DownloadedBundle {
|
|
42
|
+
id: string;
|
|
43
|
+
version: string;
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/OtaUpdater.d.ts
|
|
47
|
+
type EventCallback = (data?: any) => void;
|
|
48
|
+
declare class OtaUpdater {
|
|
49
|
+
private config;
|
|
50
|
+
private listeners;
|
|
51
|
+
private downloadedBundle;
|
|
52
|
+
state: OtaState;
|
|
53
|
+
constructor(config: OtaConfig);
|
|
54
|
+
on(event: string, callback: EventCallback): void;
|
|
55
|
+
off(event: string, callback: EventCallback): void;
|
|
56
|
+
private emit;
|
|
57
|
+
private setState;
|
|
58
|
+
notifyAppReady(): Promise<void>;
|
|
59
|
+
check(): Promise<boolean>;
|
|
60
|
+
download(): Promise<boolean>;
|
|
61
|
+
apply(): Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
declare function getOtaUpdater(): OtaUpdater | null;
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/OtaUpdateElement.d.ts
|
|
66
|
+
declare class OtaUpdateElement extends HTMLElement {
|
|
67
|
+
private shadow;
|
|
68
|
+
private texts;
|
|
69
|
+
private theme?;
|
|
70
|
+
private visible;
|
|
71
|
+
constructor();
|
|
72
|
+
setTexts(texts?: OtaTexts): void;
|
|
73
|
+
setTheme(theme?: OtaTheme): void;
|
|
74
|
+
show(): void;
|
|
75
|
+
hide(): void;
|
|
76
|
+
connectedCallback(): void;
|
|
77
|
+
private getProgressOffset;
|
|
78
|
+
private render;
|
|
79
|
+
private renderContent;
|
|
80
|
+
private attachEventListeners;
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/index.d.ts
|
|
84
|
+
declare function initOtaUpdate(config: OtaConfig): Promise<OtaUpdater>;
|
|
85
|
+
//#endregion
|
|
86
|
+
export { DownloadedBundle, OtaConfig, OtaState, OtaStatus, OtaTexts, OtaTheme, OtaUpdateElement, OtaUpdater, RemoteVersion, getOtaUpdater, initOtaUpdate };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
//#region src/utils.ts
|
|
2
|
+
function compareVersions(a, b) {
|
|
3
|
+
const pa = a.split(".").map(Number);
|
|
4
|
+
const pb = b.split(".").map(Number);
|
|
5
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
6
|
+
const na = pa[i] || 0;
|
|
7
|
+
const nb = pb[i] || 0;
|
|
8
|
+
if (na > nb) return 1;
|
|
9
|
+
if (na < nb) return -1;
|
|
10
|
+
}
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
function isNativePlatform() {
|
|
14
|
+
try {
|
|
15
|
+
return typeof Capacitor !== "undefined" && Capacitor.isNativePlatform();
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/OtaUpdater.ts
|
|
23
|
+
var OtaUpdater = class {
|
|
24
|
+
config;
|
|
25
|
+
listeners = /* @__PURE__ */ new Map();
|
|
26
|
+
downloadedBundle = null;
|
|
27
|
+
state = {
|
|
28
|
+
status: "idle",
|
|
29
|
+
progress: 0,
|
|
30
|
+
currentVersion: "",
|
|
31
|
+
newVersion: null,
|
|
32
|
+
error: null
|
|
33
|
+
};
|
|
34
|
+
constructor(config) {
|
|
35
|
+
this.config = config;
|
|
36
|
+
this.state.currentVersion = config.appVersion;
|
|
37
|
+
}
|
|
38
|
+
on(event, callback) {
|
|
39
|
+
if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
40
|
+
this.listeners.get(event).add(callback);
|
|
41
|
+
}
|
|
42
|
+
off(event, callback) {
|
|
43
|
+
this.listeners.get(event)?.delete(callback);
|
|
44
|
+
}
|
|
45
|
+
emit(event, data) {
|
|
46
|
+
this.listeners.get(event)?.forEach((cb) => cb(data));
|
|
47
|
+
}
|
|
48
|
+
setState(partial) {
|
|
49
|
+
Object.assign(this.state, partial);
|
|
50
|
+
this.emit("stateChange", { ...this.state });
|
|
51
|
+
if (partial.progress !== void 0) this.emit("progress", partial.progress);
|
|
52
|
+
if (partial.error) this.emit("error", partial.error);
|
|
53
|
+
}
|
|
54
|
+
async notifyAppReady() {
|
|
55
|
+
if (!isNativePlatform()) return;
|
|
56
|
+
try {
|
|
57
|
+
const { CapacitorUpdater } = await import("@capgo/capacitor-updater");
|
|
58
|
+
await CapacitorUpdater.notifyAppReady();
|
|
59
|
+
} catch {}
|
|
60
|
+
}
|
|
61
|
+
async check() {
|
|
62
|
+
this.setState({
|
|
63
|
+
status: "checking",
|
|
64
|
+
error: null
|
|
65
|
+
});
|
|
66
|
+
try {
|
|
67
|
+
const url = `${this.config.staticUrl}/updates/${this.config.appId}.json?_=${Date.now()}`;
|
|
68
|
+
const response = await fetch(url);
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
if (response.status === 404) {
|
|
71
|
+
this.setState({ status: "idle" });
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
throw new Error(`HTTP ${response.status}`);
|
|
75
|
+
}
|
|
76
|
+
const data = await response.json();
|
|
77
|
+
if (compareVersions(data.version, this.config.appVersion) > 0) {
|
|
78
|
+
this.setState({ newVersion: data.version });
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
this.setState({ status: "idle" });
|
|
82
|
+
return false;
|
|
83
|
+
} catch (e) {
|
|
84
|
+
const error = e instanceof Error ? e.message : "Update check failed";
|
|
85
|
+
this.setState({
|
|
86
|
+
status: "error",
|
|
87
|
+
error
|
|
88
|
+
});
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async download() {
|
|
93
|
+
if (!this.state.newVersion) {
|
|
94
|
+
this.setState({
|
|
95
|
+
status: "error",
|
|
96
|
+
error: "No update info available"
|
|
97
|
+
});
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
this.setState({
|
|
101
|
+
status: "downloading",
|
|
102
|
+
progress: 0,
|
|
103
|
+
error: null
|
|
104
|
+
});
|
|
105
|
+
if (!isNativePlatform()) {
|
|
106
|
+
for (let i = 0; i <= 100; i += 5) {
|
|
107
|
+
this.setState({ progress: i });
|
|
108
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
109
|
+
}
|
|
110
|
+
this.downloadedBundle = {
|
|
111
|
+
id: "web-sim",
|
|
112
|
+
version: this.state.newVersion
|
|
113
|
+
};
|
|
114
|
+
this.setState({ status: "ready" });
|
|
115
|
+
this.emit("updateReady");
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const url = `${this.config.staticUrl}/updates/${this.config.appId}.json`;
|
|
120
|
+
const data = await (await fetch(url)).json();
|
|
121
|
+
const { CapacitorUpdater } = await import("@capgo/capacitor-updater");
|
|
122
|
+
const listener = await CapacitorUpdater.addListener("download", (event) => {
|
|
123
|
+
this.setState({ progress: event.percent });
|
|
124
|
+
});
|
|
125
|
+
const bundle = await CapacitorUpdater.download({
|
|
126
|
+
url: data.url,
|
|
127
|
+
version: data.version,
|
|
128
|
+
checksum: data.checksum
|
|
129
|
+
});
|
|
130
|
+
listener.remove();
|
|
131
|
+
this.downloadedBundle = {
|
|
132
|
+
id: bundle.id,
|
|
133
|
+
version: bundle.version
|
|
134
|
+
};
|
|
135
|
+
this.setState({ status: "ready" });
|
|
136
|
+
this.emit("updateReady");
|
|
137
|
+
return true;
|
|
138
|
+
} catch (e) {
|
|
139
|
+
const error = e instanceof Error ? e.message : "Download failed";
|
|
140
|
+
this.setState({
|
|
141
|
+
status: "error",
|
|
142
|
+
error
|
|
143
|
+
});
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async apply() {
|
|
148
|
+
this.setState({ status: "applying" });
|
|
149
|
+
if (!isNativePlatform()) {
|
|
150
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
151
|
+
this.emit("updateApplied");
|
|
152
|
+
window.location.reload();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (!this.downloadedBundle) {
|
|
156
|
+
this.setState({
|
|
157
|
+
status: "error",
|
|
158
|
+
error: "No downloaded bundle"
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const { CapacitorUpdater } = await import("@capgo/capacitor-updater");
|
|
164
|
+
await CapacitorUpdater.set({ id: this.downloadedBundle.id });
|
|
165
|
+
this.emit("updateApplied");
|
|
166
|
+
} catch (e) {
|
|
167
|
+
const error = e instanceof Error ? e.message : "Apply failed";
|
|
168
|
+
this.setState({
|
|
169
|
+
status: "error",
|
|
170
|
+
error
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
let instance = null;
|
|
176
|
+
function getOtaUpdater() {
|
|
177
|
+
return instance;
|
|
178
|
+
}
|
|
179
|
+
function createOtaUpdater(config) {
|
|
180
|
+
instance = new OtaUpdater(config);
|
|
181
|
+
return instance;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region src/styles.ts
|
|
186
|
+
function getStyles(theme) {
|
|
187
|
+
return `
|
|
188
|
+
:host {
|
|
189
|
+
--ota-primary: ${theme?.primaryColor || "#10b981"};
|
|
190
|
+
--ota-gradient-from: ${theme?.gradientFrom || "#10b981"};
|
|
191
|
+
--ota-gradient-to: ${theme?.gradientTo || "#059669"};
|
|
192
|
+
--ota-text: ${theme?.textColor || "#ffffff"};
|
|
193
|
+
--ota-text-muted: rgba(255, 255, 255, 0.7);
|
|
194
|
+
--ota-overlay-bg: linear-gradient(to bottom, var(--ota-gradient-from), var(--ota-gradient-to));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
* {
|
|
198
|
+
margin: 0;
|
|
199
|
+
padding: 0;
|
|
200
|
+
box-sizing: border-box;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.overlay {
|
|
204
|
+
position: fixed;
|
|
205
|
+
inset: 0;
|
|
206
|
+
z-index: 9999;
|
|
207
|
+
background: var(--ota-overlay-bg);
|
|
208
|
+
display: flex;
|
|
209
|
+
flex-direction: column;
|
|
210
|
+
align-items: center;
|
|
211
|
+
justify-content: center;
|
|
212
|
+
padding: 2rem;
|
|
213
|
+
color: var(--ota-text);
|
|
214
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
215
|
+
animation: fadeIn 0.3s ease;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.overlay.hidden {
|
|
219
|
+
display: none;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
@keyframes fadeIn {
|
|
223
|
+
from { opacity: 0; }
|
|
224
|
+
to { opacity: 1; }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.safe-area-top {
|
|
228
|
+
padding-top: env(safe-area-inset-top);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.safe-area-bottom {
|
|
232
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.container {
|
|
236
|
+
display: flex;
|
|
237
|
+
flex-direction: column;
|
|
238
|
+
align-items: center;
|
|
239
|
+
text-align: center;
|
|
240
|
+
width: 100%;
|
|
241
|
+
max-width: 320px;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.icon {
|
|
245
|
+
width: 96px;
|
|
246
|
+
height: 96px;
|
|
247
|
+
background: white;
|
|
248
|
+
border-radius: 22px;
|
|
249
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
250
|
+
display: flex;
|
|
251
|
+
align-items: center;
|
|
252
|
+
justify-content: center;
|
|
253
|
+
margin-bottom: 2rem;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.icon svg {
|
|
257
|
+
width: 56px;
|
|
258
|
+
height: 56px;
|
|
259
|
+
color: var(--ota-primary);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
h1 {
|
|
263
|
+
font-size: 1.5rem;
|
|
264
|
+
font-weight: 700;
|
|
265
|
+
margin-bottom: 0.5rem;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.version {
|
|
269
|
+
color: var(--ota-text-muted);
|
|
270
|
+
margin-bottom: 2rem;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.content {
|
|
274
|
+
width: 100%;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/* Progress Circle */
|
|
278
|
+
.progress-container {
|
|
279
|
+
position: relative;
|
|
280
|
+
width: 128px;
|
|
281
|
+
height: 128px;
|
|
282
|
+
margin: 0 auto 1.5rem;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.progress-svg {
|
|
286
|
+
width: 100%;
|
|
287
|
+
height: 100%;
|
|
288
|
+
transform: rotate(-90deg);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.progress-bg {
|
|
292
|
+
fill: none;
|
|
293
|
+
stroke: rgba(255, 255, 255, 0.2);
|
|
294
|
+
stroke-width: 8;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.progress-bar {
|
|
298
|
+
fill: none;
|
|
299
|
+
stroke: white;
|
|
300
|
+
stroke-width: 8;
|
|
301
|
+
stroke-linecap: round;
|
|
302
|
+
transition: stroke-dashoffset 0.3s ease;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.progress-text {
|
|
306
|
+
position: absolute;
|
|
307
|
+
inset: 0;
|
|
308
|
+
display: flex;
|
|
309
|
+
align-items: center;
|
|
310
|
+
justify-content: center;
|
|
311
|
+
font-size: 1.875rem;
|
|
312
|
+
font-weight: 700;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.status-text {
|
|
316
|
+
color: var(--ota-text-muted);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/* Ready State */
|
|
320
|
+
.check-icon {
|
|
321
|
+
width: 64px;
|
|
322
|
+
height: 64px;
|
|
323
|
+
background: rgba(255, 255, 255, 0.2);
|
|
324
|
+
border-radius: 50%;
|
|
325
|
+
display: flex;
|
|
326
|
+
align-items: center;
|
|
327
|
+
justify-content: center;
|
|
328
|
+
margin: 0 auto 1.5rem;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.check-icon svg {
|
|
332
|
+
width: 32px;
|
|
333
|
+
height: 32px;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.apply-btn {
|
|
337
|
+
width: 100%;
|
|
338
|
+
padding: 1rem;
|
|
339
|
+
background: white;
|
|
340
|
+
color: var(--ota-primary);
|
|
341
|
+
font-size: 1rem;
|
|
342
|
+
font-weight: 600;
|
|
343
|
+
border: none;
|
|
344
|
+
border-radius: 1rem;
|
|
345
|
+
cursor: pointer;
|
|
346
|
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
|
347
|
+
transition: transform 0.15s ease;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.apply-btn:active {
|
|
351
|
+
transform: scale(0.95);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/* Applying State */
|
|
355
|
+
.spinner {
|
|
356
|
+
width: 32px;
|
|
357
|
+
height: 32px;
|
|
358
|
+
border: 3px solid rgba(255, 255, 255, 0.3);
|
|
359
|
+
border-top-color: white;
|
|
360
|
+
border-radius: 50%;
|
|
361
|
+
margin: 0 auto 1rem;
|
|
362
|
+
animation: spin 0.8s linear infinite;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
@keyframes spin {
|
|
366
|
+
to { transform: rotate(360deg); }
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/* Error State */
|
|
370
|
+
.error-icon {
|
|
371
|
+
width: 64px;
|
|
372
|
+
height: 64px;
|
|
373
|
+
background: rgba(239, 68, 68, 0.2);
|
|
374
|
+
border-radius: 50%;
|
|
375
|
+
display: flex;
|
|
376
|
+
align-items: center;
|
|
377
|
+
justify-content: center;
|
|
378
|
+
margin: 0 auto 1.5rem;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.error-icon svg {
|
|
382
|
+
width: 32px;
|
|
383
|
+
height: 32px;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.error-title {
|
|
387
|
+
color: #fecaca;
|
|
388
|
+
margin-bottom: 0.5rem;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.error-message {
|
|
392
|
+
color: var(--ota-text-muted);
|
|
393
|
+
font-size: 0.875rem;
|
|
394
|
+
margin-bottom: 1.5rem;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.retry-btn {
|
|
398
|
+
width: 100%;
|
|
399
|
+
padding: 1rem;
|
|
400
|
+
background: rgba(255, 255, 255, 0.2);
|
|
401
|
+
color: white;
|
|
402
|
+
font-size: 1rem;
|
|
403
|
+
font-weight: 600;
|
|
404
|
+
border: none;
|
|
405
|
+
border-radius: 1rem;
|
|
406
|
+
cursor: pointer;
|
|
407
|
+
transition: transform 0.15s ease;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.retry-btn:active {
|
|
411
|
+
transform: scale(0.95);
|
|
412
|
+
}
|
|
413
|
+
`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
//#endregion
|
|
417
|
+
//#region src/OtaUpdateElement.ts
|
|
418
|
+
const DEFAULT_TEXTS = {
|
|
419
|
+
title: "Güncelleme Mevcut",
|
|
420
|
+
downloading: "İndiriliyor...",
|
|
421
|
+
downloadComplete: "İndirme tamamlandı!",
|
|
422
|
+
applyButton: "Şimdi Güncelle",
|
|
423
|
+
applying: "Güncelleme uygulanıyor...",
|
|
424
|
+
errorTitle: "Güncelleme başarısız",
|
|
425
|
+
retryButton: "Tekrar Dene"
|
|
426
|
+
};
|
|
427
|
+
var OtaUpdateElement = class extends HTMLElement {
|
|
428
|
+
shadow;
|
|
429
|
+
texts;
|
|
430
|
+
theme;
|
|
431
|
+
visible = false;
|
|
432
|
+
constructor() {
|
|
433
|
+
super();
|
|
434
|
+
this.shadow = this.attachShadow({ mode: "open" });
|
|
435
|
+
this.texts = { ...DEFAULT_TEXTS };
|
|
436
|
+
}
|
|
437
|
+
setTexts(texts) {
|
|
438
|
+
this.texts = {
|
|
439
|
+
...DEFAULT_TEXTS,
|
|
440
|
+
...texts
|
|
441
|
+
};
|
|
442
|
+
this.render();
|
|
443
|
+
}
|
|
444
|
+
setTheme(theme) {
|
|
445
|
+
this.theme = theme;
|
|
446
|
+
this.render();
|
|
447
|
+
}
|
|
448
|
+
show() {
|
|
449
|
+
this.visible = true;
|
|
450
|
+
this.render();
|
|
451
|
+
}
|
|
452
|
+
hide() {
|
|
453
|
+
this.visible = false;
|
|
454
|
+
this.render();
|
|
455
|
+
}
|
|
456
|
+
connectedCallback() {
|
|
457
|
+
const ota = getOtaUpdater();
|
|
458
|
+
if (ota) ota.on("stateChange", () => this.render());
|
|
459
|
+
this.render();
|
|
460
|
+
}
|
|
461
|
+
getProgressOffset(progress) {
|
|
462
|
+
const circumference = 2 * Math.PI * 45;
|
|
463
|
+
return circumference - circumference * progress / 100;
|
|
464
|
+
}
|
|
465
|
+
render() {
|
|
466
|
+
const state = getOtaUpdater()?.state || {
|
|
467
|
+
status: "idle",
|
|
468
|
+
progress: 0,
|
|
469
|
+
currentVersion: "",
|
|
470
|
+
newVersion: null,
|
|
471
|
+
error: null
|
|
472
|
+
};
|
|
473
|
+
const circumference = 2 * Math.PI * 45;
|
|
474
|
+
this.shadow.innerHTML = `
|
|
475
|
+
<style>${getStyles(this.theme)}</style>
|
|
476
|
+
<div class="overlay ${this.visible ? "" : "hidden"}">
|
|
477
|
+
<div class="safe-area-top"></div>
|
|
478
|
+
<div class="container">
|
|
479
|
+
<!-- App Icon -->
|
|
480
|
+
<div class="icon">
|
|
481
|
+
<svg viewBox="0 0 24 24" fill="currentColor">
|
|
482
|
+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>
|
|
483
|
+
</svg>
|
|
484
|
+
</div>
|
|
485
|
+
|
|
486
|
+
<!-- Title -->
|
|
487
|
+
<h1>${this.texts.title}</h1>
|
|
488
|
+
|
|
489
|
+
<!-- Version -->
|
|
490
|
+
<p class="version">v${state.currentVersion} → v${state.newVersion || "..."}</p>
|
|
491
|
+
|
|
492
|
+
<!-- Content -->
|
|
493
|
+
<div class="content">
|
|
494
|
+
${this.renderContent(state, circumference)}
|
|
495
|
+
</div>
|
|
496
|
+
</div>
|
|
497
|
+
<div class="safe-area-bottom"></div>
|
|
498
|
+
</div>
|
|
499
|
+
`;
|
|
500
|
+
this.attachEventListeners();
|
|
501
|
+
}
|
|
502
|
+
renderContent(state, circumference) {
|
|
503
|
+
switch (state.status) {
|
|
504
|
+
case "downloading": return `
|
|
505
|
+
<div class="progress-container">
|
|
506
|
+
<svg class="progress-svg" viewBox="0 0 100 100">
|
|
507
|
+
<circle class="progress-bg" cx="50" cy="50" r="45" />
|
|
508
|
+
<circle
|
|
509
|
+
class="progress-bar"
|
|
510
|
+
cx="50" cy="50" r="45"
|
|
511
|
+
stroke-dasharray="${circumference}"
|
|
512
|
+
stroke-dashoffset="${this.getProgressOffset(state.progress)}"
|
|
513
|
+
/>
|
|
514
|
+
</svg>
|
|
515
|
+
<div class="progress-text">${state.progress}%</div>
|
|
516
|
+
</div>
|
|
517
|
+
<p class="status-text">${this.texts.downloading}</p>
|
|
518
|
+
`;
|
|
519
|
+
case "ready": return `
|
|
520
|
+
<div class="check-icon">
|
|
521
|
+
<svg fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
|
522
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
|
523
|
+
</svg>
|
|
524
|
+
</div>
|
|
525
|
+
<p class="status-text" style="margin-bottom: 1.5rem;">${this.texts.downloadComplete}</p>
|
|
526
|
+
<button class="apply-btn" data-action="apply">${this.texts.applyButton}</button>
|
|
527
|
+
`;
|
|
528
|
+
case "applying": return `
|
|
529
|
+
<div class="spinner"></div>
|
|
530
|
+
<p class="status-text">${this.texts.applying}</p>
|
|
531
|
+
`;
|
|
532
|
+
case "error": return `
|
|
533
|
+
<div class="error-icon">
|
|
534
|
+
<svg fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
|
535
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
536
|
+
</svg>
|
|
537
|
+
</div>
|
|
538
|
+
<p class="error-title">${this.texts.errorTitle}</p>
|
|
539
|
+
<p class="error-message">${state.error || ""}</p>
|
|
540
|
+
<button class="retry-btn" data-action="retry">${this.texts.retryButton}</button>
|
|
541
|
+
`;
|
|
542
|
+
default: return "";
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
attachEventListeners() {
|
|
546
|
+
const ota = getOtaUpdater();
|
|
547
|
+
if (!ota) return;
|
|
548
|
+
const applyBtn = this.shadow.querySelector("[data-action=\"apply\"]");
|
|
549
|
+
const retryBtn = this.shadow.querySelector("[data-action=\"retry\"]");
|
|
550
|
+
applyBtn?.addEventListener("click", () => {
|
|
551
|
+
ota.apply();
|
|
552
|
+
});
|
|
553
|
+
retryBtn?.addEventListener("click", async () => {
|
|
554
|
+
if (!await ota.download()) this.render();
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
function registerOtaUpdateElement() {
|
|
559
|
+
if (!customElements.get("ota-update")) customElements.define("ota-update", OtaUpdateElement);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
//#endregion
|
|
563
|
+
//#region src/index.ts
|
|
564
|
+
async function initOtaUpdate(config) {
|
|
565
|
+
const ota = createOtaUpdater(config);
|
|
566
|
+
if (config.showUI !== false) {
|
|
567
|
+
registerOtaUpdateElement();
|
|
568
|
+
const element = document.createElement("ota-update");
|
|
569
|
+
element.setTheme(config.theme);
|
|
570
|
+
element.setTexts(config.texts);
|
|
571
|
+
document.body.appendChild(element);
|
|
572
|
+
await ota.notifyAppReady();
|
|
573
|
+
if (await ota.check()) {
|
|
574
|
+
element.show();
|
|
575
|
+
if (await ota.download() && config.autoApply) await ota.apply();
|
|
576
|
+
}
|
|
577
|
+
} else await ota.notifyAppReady();
|
|
578
|
+
return ota;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
//#endregion
|
|
582
|
+
export { OtaUpdateElement, OtaUpdater, getOtaUpdater, initOtaUpdate };
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "capacitor-ota",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.mjs",
|
|
6
|
+
"types": "dist/index.d.mts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.mjs",
|
|
10
|
+
"types": "./dist/index.d.mts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsdown src/index.ts --format esm --dts",
|
|
18
|
+
"dev": "tsdown src/index.ts --format esm --dts --watch",
|
|
19
|
+
"panel:dev": "vite dev --host",
|
|
20
|
+
"panel:build": "vite build",
|
|
21
|
+
"panel:preview": "vite preview",
|
|
22
|
+
"db:migrate": "wrangler d1 migrations apply ota-db --local",
|
|
23
|
+
"db:migrate:remote": "wrangler d1 migrations apply ota-db --remote",
|
|
24
|
+
"cli": "tsx cli/ota.ts"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@capgo/capacitor-updater": ">=6.0.0",
|
|
28
|
+
"@capacitor/core": ">=8.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@cloudflare/workers-types": "^4.20260123.0",
|
|
32
|
+
"@tailwindcss/vite": "^4.1.18",
|
|
33
|
+
"@vitejs/plugin-vue": "^6.0.3",
|
|
34
|
+
"nitro": "3.0.1-alpha.2",
|
|
35
|
+
"pinia": "^3.0.4",
|
|
36
|
+
"tailwindcss": "^4.1.18",
|
|
37
|
+
"tsdown": "^0.20.1",
|
|
38
|
+
"tsx": "^4.21.0",
|
|
39
|
+
"typescript": "^5.9.3",
|
|
40
|
+
"vite": "8.0.0-beta.8",
|
|
41
|
+
"vue": "^3.5.27",
|
|
42
|
+
"vue-router": "^4.6.4",
|
|
43
|
+
"wrangler": "^4.60.0"
|
|
44
|
+
}
|
|
45
|
+
}
|