termcast 1.3.33 → 1.3.35
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/apis/cache.d.ts +1 -2
- package/dist/apis/cache.d.ts.map +1 -1
- package/dist/apis/cache.js +134 -52
- package/dist/apis/cache.js.map +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +25 -0
- package/dist/build.js.map +1 -1
- package/dist/cli.js +6 -8
- package/dist/cli.js.map +1 -1
- package/dist/components/dropdown.js +3 -3
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/footer.d.ts.map +1 -1
- package/dist/components/footer.js +1 -1
- package/dist/components/footer.js.map +1 -1
- package/dist/components/icon.d.ts.map +1 -1
- package/dist/components/icon.js +386 -23
- package/dist/components/icon.js.map +1 -1
- package/dist/components/list.d.ts.map +1 -1
- package/dist/components/list.js +90 -22
- package/dist/components/list.js.map +1 -1
- package/dist/examples/list-controlled-search.d.ts +2 -0
- package/dist/examples/list-controlled-search.d.ts.map +1 -0
- package/dist/examples/list-controlled-search.js +12 -0
- package/dist/examples/list-controlled-search.js.map +1 -0
- package/dist/extensions/home.js +1 -1
- package/dist/extensions/home.js.map +1 -1
- package/dist/extensions/react-refresh-init.d.ts.map +1 -1
- package/dist/extensions/react-refresh-init.js +4 -3
- package/dist/extensions/react-refresh-init.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/dialog.d.ts.map +1 -1
- package/dist/internal/dialog.js +4 -5
- package/dist/internal/dialog.js.map +1 -1
- package/dist/internal/providers.d.ts.map +1 -1
- package/dist/internal/providers.js +18 -5
- package/dist/internal/providers.js.map +1 -1
- package/dist/state.d.ts +1 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js.map +1 -1
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +6 -2
- package/dist/theme.js.map +1 -1
- package/dist/utils/run-command.js +3 -3
- package/dist/utils/run-command.js.map +1 -1
- package/dist/utils.d.ts +16 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +28 -1
- package/dist/utils.js.map +1 -1
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +24 -4
- package/dist/watcher.js.map +1 -1
- package/package.json +10 -9
- package/src/apis/cache.test.ts +35 -3
- package/src/apis/cache.tsx +180 -57
- package/src/build.tsx +28 -0
- package/src/cli.tsx +8 -10
- package/src/compile.vitest.tsx +42 -24
- package/src/components/dropdown.tsx +3 -3
- package/src/components/footer.tsx +4 -2
- package/src/components/icon.tsx +385 -23
- package/src/components/list.tsx +104 -28
- package/src/examples/github.vitest.tsx +37 -37
- package/src/examples/list-controlled-search.tsx +28 -0
- package/src/examples/list-controlled-search.vitest.tsx +49 -0
- package/src/examples/list-detail-metadata.vitest.tsx +1 -1
- package/src/examples/list-dropdown-default.vitest.tsx +9 -9
- package/src/examples/list-scrollbox.vitest.tsx +55 -41
- package/src/examples/list-with-detail.vitest.tsx +35 -36
- package/src/examples/list-with-dropdown.vitest.tsx +2 -2
- package/src/examples/list-with-sections.vitest.tsx +153 -118
- package/src/examples/simple-file-picker.vitest.tsx +1 -1
- package/src/examples/simple-grid.vitest.tsx +44 -44
- package/src/examples/simple-navigation.vitest.tsx +43 -12
- package/src/examples/store.vitest.tsx +1 -1
- package/src/examples/swift-extension.vitest.tsx +3 -3
- package/src/extensions/dev.vitest.tsx +69 -34
- package/src/extensions/home.tsx +1 -1
- package/src/extensions/react-refresh-init.tsx +4 -3
- package/src/index.tsx +1 -0
- package/src/internal/dialog.tsx +21 -23
- package/src/internal/providers.tsx +18 -5
- package/src/state.tsx +1 -0
- package/src/theme.tsx +6 -2
- package/src/utils/run-command.tsx +3 -3
- package/src/utils.tsx +41 -1
- package/src/watcher.tsx +26 -6
package/src/apis/cache.tsx
CHANGED
|
@@ -5,6 +5,16 @@ import * as fs from 'fs'
|
|
|
5
5
|
import { logger } from '../logger'
|
|
6
6
|
import { useStore } from '../state'
|
|
7
7
|
|
|
8
|
+
const CACHE_TABLE_NAME = 'cache_entries'
|
|
9
|
+
const DEFAULT_NAMESPACE = '__default__'
|
|
10
|
+
const initializedDatabasePaths = new Set<string>()
|
|
11
|
+
let logicalTimestamp = Date.now()
|
|
12
|
+
|
|
13
|
+
function nextTimestamp(): number {
|
|
14
|
+
logicalTimestamp += 1
|
|
15
|
+
return logicalTimestamp
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
function getCurrentDatabasePath(): string {
|
|
9
19
|
const { extensionPath } = useStore.getState()
|
|
10
20
|
const dbSuffix = process.env.TERMCAST_DB_SUFFIX?.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
@@ -28,6 +38,109 @@ function getCurrentCacheDir(namespace?: string): string {
|
|
|
28
38
|
return namespace ? path.join(baseDir, namespace) : baseDir
|
|
29
39
|
}
|
|
30
40
|
|
|
41
|
+
function getNamespace(namespace?: string): string {
|
|
42
|
+
return namespace || DEFAULT_NAMESPACE
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function initializeDatabaseOnce({ db, dbPath }: { db: Database; dbPath: string }): void {
|
|
46
|
+
if (initializedDatabasePaths.has(dbPath)) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
db.exec('PRAGMA journal_mode = WAL')
|
|
51
|
+
db.exec('PRAGMA wal_autocheckpoint = 1000')
|
|
52
|
+
db.exec('PRAGMA synchronous = NORMAL')
|
|
53
|
+
|
|
54
|
+
db.exec(`
|
|
55
|
+
CREATE TABLE IF NOT EXISTS ${CACHE_TABLE_NAME} (
|
|
56
|
+
namespace TEXT NOT NULL,
|
|
57
|
+
key TEXT NOT NULL,
|
|
58
|
+
data TEXT NOT NULL,
|
|
59
|
+
size INTEGER NOT NULL,
|
|
60
|
+
last_accessed_at INTEGER NOT NULL,
|
|
61
|
+
updated_at INTEGER NOT NULL,
|
|
62
|
+
PRIMARY KEY(namespace, key)
|
|
63
|
+
)
|
|
64
|
+
`)
|
|
65
|
+
|
|
66
|
+
db.exec(`
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_${CACHE_TABLE_NAME}_namespace_lru
|
|
68
|
+
ON ${CACHE_TABLE_NAME}(namespace, last_accessed_at)
|
|
69
|
+
`)
|
|
70
|
+
|
|
71
|
+
cleanupLegacyCacheTables(db)
|
|
72
|
+
initializedDatabasePaths.add(dbPath)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function cleanupLegacyCacheTables(db: Database): void {
|
|
76
|
+
const rows = db
|
|
77
|
+
.prepare(
|
|
78
|
+
`SELECT name FROM sqlite_master
|
|
79
|
+
WHERE type = 'table'
|
|
80
|
+
AND (name = 'cache' OR name LIKE 'cache_%')
|
|
81
|
+
AND name != ?`,
|
|
82
|
+
)
|
|
83
|
+
.all(CACHE_TABLE_NAME) as Array<{ name: string }>
|
|
84
|
+
|
|
85
|
+
if (rows.length === 0) {
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const tx = db.transaction(() => {
|
|
90
|
+
const upsert = db.prepare(
|
|
91
|
+
`INSERT INTO ${CACHE_TABLE_NAME} (namespace, key, data, size, last_accessed_at, updated_at)
|
|
92
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
93
|
+
ON CONFLICT(namespace, key)
|
|
94
|
+
DO UPDATE SET
|
|
95
|
+
data = excluded.data,
|
|
96
|
+
size = excluded.size,
|
|
97
|
+
last_accessed_at = excluded.last_accessed_at,
|
|
98
|
+
updated_at = excluded.updated_at`,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
for (const { name } of rows) {
|
|
102
|
+
const namespace =
|
|
103
|
+
name === 'cache'
|
|
104
|
+
? DEFAULT_NAMESPACE
|
|
105
|
+
: name === 'cache_tanstack_query'
|
|
106
|
+
? 'tanstack-query'
|
|
107
|
+
: `legacy:${name.slice('cache_'.length)}`
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const values = db
|
|
111
|
+
.prepare(`SELECT key, data, size, rowid FROM ${name}`)
|
|
112
|
+
.all() as Array<{ key: string; data: string; size: number; rowid: number }>
|
|
113
|
+
|
|
114
|
+
values.forEach((entry) => {
|
|
115
|
+
const timestamp = entry.rowid
|
|
116
|
+
upsert.run(
|
|
117
|
+
namespace,
|
|
118
|
+
entry.key,
|
|
119
|
+
entry.data,
|
|
120
|
+
entry.size,
|
|
121
|
+
timestamp,
|
|
122
|
+
timestamp,
|
|
123
|
+
)
|
|
124
|
+
})
|
|
125
|
+
} catch {
|
|
126
|
+
// Ignore invalid legacy tables and continue cleanup.
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
db.exec(`DROP TABLE IF EXISTS ${name}`)
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
tx()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function hashString(value: string): string {
|
|
137
|
+
let hash = 0
|
|
138
|
+
for (let i = 0; i < value.length; i++) {
|
|
139
|
+
hash = (hash * 31 + value.charCodeAt(i)) | 0
|
|
140
|
+
}
|
|
141
|
+
return Math.abs(hash).toString(36)
|
|
142
|
+
}
|
|
143
|
+
|
|
31
144
|
export class Cache {
|
|
32
145
|
static get STORAGE_DIRECTORY_NAME(): string {
|
|
33
146
|
const extensionPath = useStore.getState().extensionPath
|
|
@@ -40,17 +153,14 @@ export class Cache {
|
|
|
40
153
|
|
|
41
154
|
private db: Database
|
|
42
155
|
private capacity: number
|
|
43
|
-
private namespace
|
|
44
|
-
private tableName: string
|
|
156
|
+
private namespace: string
|
|
45
157
|
private subscribers: Cache.Subscriber[] = []
|
|
46
158
|
private currentSize: number = 0
|
|
47
159
|
|
|
48
160
|
constructor(options?: Cache.Options) {
|
|
161
|
+
const sqliteLoadStart = Date.now()
|
|
49
162
|
this.capacity = options?.capacity || Cache.DEFAULT_CAPACITY
|
|
50
|
-
this.namespace = options?.namespace
|
|
51
|
-
// Replace non-alphanumeric characters with underscores for valid SQL table names
|
|
52
|
-
const safeNamespace = this.namespace?.replace(/[^a-zA-Z0-9]/g, '_')
|
|
53
|
-
this.tableName = safeNamespace ? `cache_${safeNamespace}` : 'cache'
|
|
163
|
+
this.namespace = getNamespace(options?.namespace)
|
|
54
164
|
|
|
55
165
|
const dbPath = getCurrentDatabasePath()
|
|
56
166
|
|
|
@@ -66,32 +176,23 @@ export class Cache {
|
|
|
66
176
|
readwrite: true,
|
|
67
177
|
})
|
|
68
178
|
|
|
69
|
-
|
|
70
|
-
this.db.exec('PRAGMA journal_mode = WAL')
|
|
71
|
-
this.db.exec('PRAGMA wal_autocheckpoint = 1000')
|
|
72
|
-
this.db.exec('PRAGMA synchronous = NORMAL')
|
|
73
|
-
|
|
74
|
-
// Use rowid for ordering - it auto-increments and provides natural LRU order
|
|
75
|
-
this.db.exec(`
|
|
76
|
-
CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
77
|
-
rowid INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
78
|
-
key TEXT UNIQUE NOT NULL,
|
|
79
|
-
data TEXT NOT NULL,
|
|
80
|
-
size INTEGER NOT NULL
|
|
81
|
-
)
|
|
82
|
-
`)
|
|
83
|
-
|
|
84
|
-
// Create index on key for fast lookups
|
|
85
|
-
this.db.exec(`
|
|
86
|
-
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_key ON ${this.tableName}(key)
|
|
87
|
-
`)
|
|
179
|
+
initializeDatabaseOnce({ db: this.db, dbPath })
|
|
88
180
|
|
|
89
181
|
// Calculate initial size
|
|
90
182
|
const row = this.db
|
|
91
|
-
.prepare(
|
|
92
|
-
|
|
183
|
+
.prepare(
|
|
184
|
+
`SELECT COALESCE(SUM(size), 0) as total FROM ${CACHE_TABLE_NAME} WHERE namespace = ?`,
|
|
185
|
+
)
|
|
186
|
+
.get(this.namespace) as { total: number | null } | undefined
|
|
93
187
|
this.currentSize = row?.total || 0
|
|
94
188
|
|
|
189
|
+
const sqliteLoadMs = Date.now() - sqliteLoadStart
|
|
190
|
+
if (sqliteLoadMs > 500) {
|
|
191
|
+
logger.log(
|
|
192
|
+
`[perf] sqlite cache init took ${sqliteLoadMs}ms (namespace=${this.namespace})`,
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
95
196
|
// Bind all methods to this instance
|
|
96
197
|
this.get = this.get.bind(this)
|
|
97
198
|
this.has = this.has.bind(this)
|
|
@@ -108,21 +209,19 @@ export class Cache {
|
|
|
108
209
|
}
|
|
109
210
|
|
|
110
211
|
get(key: string): string | undefined {
|
|
212
|
+
const now = nextTimestamp()
|
|
111
213
|
const row = this.db
|
|
112
|
-
.prepare(
|
|
113
|
-
|
|
214
|
+
.prepare(
|
|
215
|
+
`SELECT data, size FROM ${CACHE_TABLE_NAME} WHERE namespace = ? AND key = ?`,
|
|
216
|
+
)
|
|
217
|
+
.get(this.namespace, key) as { data: string; size: number } | undefined
|
|
114
218
|
|
|
115
219
|
if (row) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
`INSERT INTO ${this.tableName} (key, data, size) VALUES (?, ?, ?)`,
|
|
122
|
-
)
|
|
123
|
-
.run(key, row.data, row.size)
|
|
124
|
-
})
|
|
125
|
-
tx()
|
|
220
|
+
this.db
|
|
221
|
+
.prepare(
|
|
222
|
+
`UPDATE ${CACHE_TABLE_NAME} SET last_accessed_at = ? WHERE namespace = ? AND key = ?`,
|
|
223
|
+
)
|
|
224
|
+
.run(now, this.namespace, key)
|
|
126
225
|
|
|
127
226
|
return row.data
|
|
128
227
|
}
|
|
@@ -132,25 +231,30 @@ export class Cache {
|
|
|
132
231
|
|
|
133
232
|
has(key: string): boolean {
|
|
134
233
|
const row = this.db
|
|
135
|
-
.prepare(`SELECT 1 FROM ${
|
|
136
|
-
.get(key)
|
|
234
|
+
.prepare(`SELECT 1 FROM ${CACHE_TABLE_NAME} WHERE namespace = ? AND key = ?`)
|
|
235
|
+
.get(this.namespace, key)
|
|
137
236
|
return !!row
|
|
138
237
|
}
|
|
139
238
|
|
|
140
239
|
get isEmpty(): boolean {
|
|
141
240
|
const row = this.db
|
|
142
|
-
.prepare(
|
|
143
|
-
|
|
241
|
+
.prepare(
|
|
242
|
+
`SELECT COUNT(*) as count FROM ${CACHE_TABLE_NAME} WHERE namespace = ?`,
|
|
243
|
+
)
|
|
244
|
+
.get(this.namespace) as { count: number }
|
|
144
245
|
return row.count === 0
|
|
145
246
|
}
|
|
146
247
|
|
|
147
248
|
set(key: string, data: string): void {
|
|
249
|
+
const now = nextTimestamp()
|
|
148
250
|
const dataSize = Buffer.byteLength(data, 'utf8')
|
|
149
251
|
|
|
150
252
|
// Get existing size if any
|
|
151
253
|
const existingRow = this.db
|
|
152
|
-
.prepare(
|
|
153
|
-
|
|
254
|
+
.prepare(
|
|
255
|
+
`SELECT size FROM ${CACHE_TABLE_NAME} WHERE namespace = ? AND key = ?`,
|
|
256
|
+
)
|
|
257
|
+
.get(this.namespace, key) as { size: number } | undefined
|
|
154
258
|
const oldSize = existingRow?.size || 0
|
|
155
259
|
const newTotalSize = this.currentSize - oldSize + dataSize
|
|
156
260
|
|
|
@@ -161,9 +265,16 @@ export class Cache {
|
|
|
161
265
|
// Insert or update the cache entry
|
|
162
266
|
this.db
|
|
163
267
|
.prepare(
|
|
164
|
-
`INSERT
|
|
268
|
+
`INSERT INTO ${CACHE_TABLE_NAME} (namespace, key, data, size, last_accessed_at, updated_at)
|
|
269
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
270
|
+
ON CONFLICT(namespace, key)
|
|
271
|
+
DO UPDATE SET
|
|
272
|
+
data = excluded.data,
|
|
273
|
+
size = excluded.size,
|
|
274
|
+
last_accessed_at = excluded.last_accessed_at,
|
|
275
|
+
updated_at = excluded.updated_at`,
|
|
165
276
|
)
|
|
166
|
-
.run(key, data, dataSize)
|
|
277
|
+
.run(this.namespace, key, data, dataSize, now, now)
|
|
167
278
|
|
|
168
279
|
this.currentSize = this.currentSize - oldSize + dataSize
|
|
169
280
|
this.notifySubscribers(key, data)
|
|
@@ -172,12 +283,16 @@ export class Cache {
|
|
|
172
283
|
remove(key: string): boolean {
|
|
173
284
|
// Check if key exists and get its size
|
|
174
285
|
const row = this.db
|
|
175
|
-
.prepare(
|
|
176
|
-
|
|
286
|
+
.prepare(
|
|
287
|
+
`SELECT size FROM ${CACHE_TABLE_NAME} WHERE namespace = ? AND key = ?`,
|
|
288
|
+
)
|
|
289
|
+
.get(this.namespace, key) as { size: number } | undefined
|
|
177
290
|
|
|
178
291
|
if (row) {
|
|
179
292
|
// Delete the key
|
|
180
|
-
this.db
|
|
293
|
+
this.db
|
|
294
|
+
.prepare(`DELETE FROM ${CACHE_TABLE_NAME} WHERE namespace = ? AND key = ?`)
|
|
295
|
+
.run(this.namespace, key)
|
|
181
296
|
|
|
182
297
|
this.currentSize -= row.size
|
|
183
298
|
this.notifySubscribers(key, undefined)
|
|
@@ -188,7 +303,9 @@ export class Cache {
|
|
|
188
303
|
}
|
|
189
304
|
|
|
190
305
|
clear(options?: { notifySubscribers: boolean }): void {
|
|
191
|
-
this.db
|
|
306
|
+
this.db
|
|
307
|
+
.prepare(`DELETE FROM ${CACHE_TABLE_NAME} WHERE namespace = ?`)
|
|
308
|
+
.run(this.namespace)
|
|
192
309
|
this.currentSize = 0
|
|
193
310
|
|
|
194
311
|
if (options?.notifySubscribers !== false) {
|
|
@@ -207,10 +324,14 @@ export class Cache {
|
|
|
207
324
|
}
|
|
208
325
|
|
|
209
326
|
private maintainCapacity(bytesToFree: number): void {
|
|
210
|
-
// Order by
|
|
327
|
+
// Order by oldest last-access time first to evict least-recently-used rows.
|
|
211
328
|
const rows = this.db
|
|
212
|
-
.prepare(
|
|
213
|
-
|
|
329
|
+
.prepare(
|
|
330
|
+
`SELECT key, size FROM ${CACHE_TABLE_NAME}
|
|
331
|
+
WHERE namespace = ?
|
|
332
|
+
ORDER BY last_accessed_at ASC`,
|
|
333
|
+
)
|
|
334
|
+
.all(this.namespace) as Array<{ key: string; size: number }>
|
|
214
335
|
|
|
215
336
|
let freedBytes = 0
|
|
216
337
|
const keysToRemove: string[] = []
|
|
@@ -226,9 +347,10 @@ export class Cache {
|
|
|
226
347
|
if (keysToRemove.length > 0) {
|
|
227
348
|
const placeholders = keysToRemove.map(() => '?').join(',')
|
|
228
349
|
const stmt = this.db.prepare(
|
|
229
|
-
`DELETE FROM ${
|
|
350
|
+
`DELETE FROM ${CACHE_TABLE_NAME}
|
|
351
|
+
WHERE namespace = ? AND key IN (${placeholders})`,
|
|
230
352
|
)
|
|
231
|
-
stmt.run(...(keysToRemove as [string, ...string[]]))
|
|
353
|
+
stmt.run(this.namespace, ...(keysToRemove as [string, ...string[]]))
|
|
232
354
|
this.currentSize -= freedBytes
|
|
233
355
|
}
|
|
234
356
|
}
|
|
@@ -326,7 +448,8 @@ export function withCache<Fn extends (...args: any[]) => Promise<any>>(
|
|
|
326
448
|
const validate = options?.validate || (() => true)
|
|
327
449
|
|
|
328
450
|
if (!functionCacheMap.has(fnKey)) {
|
|
329
|
-
|
|
451
|
+
const functionNamespace = `fn-${hashString(fnKey)}`
|
|
452
|
+
functionCacheMap.set(fnKey, new Cache({ namespace: functionNamespace }))
|
|
330
453
|
functionCacheData.set(fnKey, new Map())
|
|
331
454
|
}
|
|
332
455
|
|
package/src/build.tsx
CHANGED
|
@@ -241,6 +241,33 @@ export async function buildExtensionCommands({
|
|
|
241
241
|
|
|
242
242
|
logger.log(`Building ${entrypoints.length} commands...`)
|
|
243
243
|
|
|
244
|
+
// Externalize the extension's declared dependencies so native modules
|
|
245
|
+
// (@libsql/client, @prisma/client, etc.) resolve from node_modules at runtime
|
|
246
|
+
// instead of being inlined into the bundle where binary addons fail to load.
|
|
247
|
+
// Only direct deps are externalized — NOT packages:'external' which would also
|
|
248
|
+
// externalize transitive deps of termcast internals (like dequal/lite) that
|
|
249
|
+
// don't exist in the extension's node_modules.
|
|
250
|
+
const rawPackageJson = JSON.parse(
|
|
251
|
+
fs.readFileSync(path.join(resolvedPath, 'package.json'), 'utf-8'),
|
|
252
|
+
)
|
|
253
|
+
const allDeps = {
|
|
254
|
+
...rawPackageJson.dependencies,
|
|
255
|
+
...rawPackageJson.devDependencies,
|
|
256
|
+
}
|
|
257
|
+
// Deps handled by aliasPlugin (mapped to globalThis), not externalized
|
|
258
|
+
const aliasedPackages = new Set([
|
|
259
|
+
'@raycast/api',
|
|
260
|
+
'@raycast/utils',
|
|
261
|
+
'react',
|
|
262
|
+
'termcast',
|
|
263
|
+
])
|
|
264
|
+
const externalDeps = Object.keys(allDeps).filter((dep) => {
|
|
265
|
+
return !aliasedPackages.has(dep)
|
|
266
|
+
})
|
|
267
|
+
if (externalDeps.length > 0) {
|
|
268
|
+
logger.log(`Externalizing ${externalDeps.length} deps: ${externalDeps.join(', ')}`)
|
|
269
|
+
}
|
|
270
|
+
|
|
244
271
|
const plugins: BunPlugin[] = [aliasPlugin, swiftLoaderPlugin]
|
|
245
272
|
|
|
246
273
|
const result = await Bun.build({
|
|
@@ -249,6 +276,7 @@ export async function buildExtensionCommands({
|
|
|
249
276
|
target: target || (format === 'cjs' ? 'node' : 'bun'),
|
|
250
277
|
format,
|
|
251
278
|
plugins,
|
|
279
|
+
external: externalDeps,
|
|
252
280
|
naming: '[name].js',
|
|
253
281
|
throw: false,
|
|
254
282
|
reactFastRefresh: hotReload,
|
package/src/cli.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import './extensions/react-refresh-init'
|
|
|
7
7
|
import fs from 'node:fs'
|
|
8
8
|
import path from 'node:path'
|
|
9
9
|
import { execSync, spawn } from 'node:child_process'
|
|
10
|
-
import {
|
|
10
|
+
import { goke } from 'goke'
|
|
11
11
|
import { getWatcher } from './watcher'
|
|
12
12
|
import { buildExtensionCommands } from './build'
|
|
13
13
|
import { logger } from './logger'
|
|
@@ -21,7 +21,7 @@ import { runHomeCommand } from './extensions/home'
|
|
|
21
21
|
import { showToast, Toast } from './apis/toast'
|
|
22
22
|
import packageJson from '../package.json'
|
|
23
23
|
|
|
24
|
-
const cli =
|
|
24
|
+
const cli = goke('termcast')
|
|
25
25
|
|
|
26
26
|
// Auto-update check
|
|
27
27
|
async function checkForUpdates() {
|
|
@@ -535,12 +535,10 @@ cli
|
|
|
535
535
|
'raycast-search <query>',
|
|
536
536
|
'Search for extensions in the Raycast store',
|
|
537
537
|
)
|
|
538
|
-
.option('-n, --limit
|
|
539
|
-
|
|
540
|
-
})
|
|
541
|
-
.action(async (query: string, options: { limit: string }) => {
|
|
538
|
+
.option('-n, --limit [number]', 'Number of results to show (default: 10)')
|
|
539
|
+
.action(async (query: string, options: { limit?: string }) => {
|
|
542
540
|
try {
|
|
543
|
-
const limit = parseInt(options.limit, 10)
|
|
541
|
+
const limit = parseInt(options.limit || '10', 10)
|
|
544
542
|
const result = await searchStoreListings({ query, perPage: limit })
|
|
545
543
|
|
|
546
544
|
if (result.data.length === 0) {
|
|
@@ -578,7 +576,7 @@ cli
|
|
|
578
576
|
'raycast-download <extensionName>',
|
|
579
577
|
'Download extension from Raycast extensions repo',
|
|
580
578
|
)
|
|
581
|
-
.option('-o, --output
|
|
579
|
+
.option('-o, --output [path]', 'Output directory (default: .)')
|
|
582
580
|
.option(
|
|
583
581
|
'--no-dir',
|
|
584
582
|
'Put files directly in output directory instead of creating extension subdirectory',
|
|
@@ -586,10 +584,10 @@ cli
|
|
|
586
584
|
.action(
|
|
587
585
|
async (
|
|
588
586
|
extensionName: string,
|
|
589
|
-
options: { output
|
|
587
|
+
options: { output?: string; dir: boolean },
|
|
590
588
|
) => {
|
|
591
589
|
try {
|
|
592
|
-
const destPath = path.resolve(options.output)
|
|
590
|
+
const destPath = path.resolve(options.output || '.')
|
|
593
591
|
// When --no-dir is passed, dir is false; put files directly in destPath
|
|
594
592
|
const extensionDir = options.dir
|
|
595
593
|
? path.join(destPath, extensionName)
|
package/src/compile.vitest.tsx
CHANGED
|
@@ -6,8 +6,14 @@ import { execSync } from 'node:child_process'
|
|
|
6
6
|
|
|
7
7
|
const fixtureDir = path.resolve(__dirname, '../fixtures/simple-extension')
|
|
8
8
|
const distDir = path.join(fixtureDir, 'dist')
|
|
9
|
+
const bundleDir = path.join(fixtureDir, '.termcast-bundle')
|
|
9
10
|
const executablePath = path.join(distDir, 'simple-extension')
|
|
10
11
|
|
|
12
|
+
const singleErrorFixtureDir = path.resolve(__dirname, '../fixtures/single-error-extension')
|
|
13
|
+
const singleErrorDistDir = path.join(singleErrorFixtureDir, 'dist')
|
|
14
|
+
const singleErrorBundleDir = path.join(singleErrorFixtureDir, '.termcast-bundle')
|
|
15
|
+
const singleErrorExecutablePath = path.join(singleErrorDistDir, 'single-error-extension')
|
|
16
|
+
|
|
11
17
|
let session: Session
|
|
12
18
|
|
|
13
19
|
afterEach(() => {
|
|
@@ -38,6 +44,22 @@ beforeAll(() => {
|
|
|
38
44
|
if (fs.existsSync(executablePath)) {
|
|
39
45
|
fs.unlinkSync(executablePath)
|
|
40
46
|
}
|
|
47
|
+
if (fs.existsSync(distDir)) {
|
|
48
|
+
fs.rmSync(distDir, { recursive: true, force: true })
|
|
49
|
+
}
|
|
50
|
+
if (fs.existsSync(bundleDir)) {
|
|
51
|
+
fs.rmSync(bundleDir, { recursive: true, force: true })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (fs.existsSync(singleErrorExecutablePath)) {
|
|
55
|
+
fs.unlinkSync(singleErrorExecutablePath)
|
|
56
|
+
}
|
|
57
|
+
if (fs.existsSync(singleErrorDistDir)) {
|
|
58
|
+
fs.rmSync(singleErrorDistDir, { recursive: true, force: true })
|
|
59
|
+
}
|
|
60
|
+
if (fs.existsSync(singleErrorBundleDir)) {
|
|
61
|
+
fs.rmSync(singleErrorBundleDir, { recursive: true, force: true })
|
|
62
|
+
}
|
|
41
63
|
})
|
|
42
64
|
|
|
43
65
|
|
|
@@ -66,12 +88,12 @@ test('compile extension and run executable', async () => {
|
|
|
66
88
|
|
|
67
89
|
> Search commands...
|
|
68
90
|
|
|
69
|
-
Commands
|
|
70
|
-
›List Items Displays a simple list with some
|
|
71
|
-
Search Items Search and filter through a list
|
|
72
|
-
Google Oauth
|
|
73
|
-
usePromise Demo Shows how to use the usePromise view
|
|
74
|
-
Show State Shows the current application state
|
|
91
|
+
Commands
|
|
92
|
+
›List Items Displays a simple list with some items view
|
|
93
|
+
Search Items Search and filter through a list of view
|
|
94
|
+
Google Oauth view
|
|
95
|
+
usePromise Demo Shows how to use the usePromise h view
|
|
96
|
+
Show State Shows the current application state in view
|
|
75
97
|
|
|
76
98
|
|
|
77
99
|
↵ run command ↑↓ navigate ^k actions
|
|
@@ -112,12 +134,12 @@ test('compiled executable can run command', async () => {
|
|
|
112
134
|
|
|
113
135
|
> Search...
|
|
114
136
|
|
|
115
|
-
Items
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
137
|
+
Items
|
|
138
|
+
›○ First Item This is the first item
|
|
139
|
+
○ Second Item This is the second item
|
|
140
|
+
○ Third Item This is the third item
|
|
141
|
+
○ Fourth Item This is the fourth item
|
|
142
|
+
○ Fifth Item This is the fifth item
|
|
121
143
|
|
|
122
144
|
|
|
123
145
|
✓ Copied to Clipboard First Item
|
|
@@ -168,12 +190,12 @@ test('compiled executable can navigate back', async () => {
|
|
|
168
190
|
|
|
169
191
|
> Search...
|
|
170
192
|
|
|
171
|
-
Items
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
193
|
+
Items
|
|
194
|
+
›○ First Item This is the first item
|
|
195
|
+
○ Second Item This is the second item
|
|
196
|
+
○ Third Item This is the third item
|
|
197
|
+
○ Fourth Item This is the fourth item
|
|
198
|
+
○ Fifth Item This is the fifth item
|
|
177
199
|
|
|
178
200
|
|
|
179
201
|
↵ copy item title ↑↓ navigate ^k actions
|
|
@@ -237,11 +259,6 @@ test('compiled executable shows error when command throws at root scope', async
|
|
|
237
259
|
`)
|
|
238
260
|
}, 60000)
|
|
239
261
|
|
|
240
|
-
// Test for single-command extension with root-level error
|
|
241
|
-
const singleErrorFixtureDir = path.resolve(__dirname, '../fixtures/single-error-extension')
|
|
242
|
-
const singleErrorDistDir = path.join(singleErrorFixtureDir, 'dist')
|
|
243
|
-
const singleErrorExecutablePath = path.join(singleErrorDistDir, 'single-error-extension')
|
|
244
|
-
|
|
245
262
|
function ensureSingleErrorCompiled() {
|
|
246
263
|
if (!fs.existsSync(singleErrorExecutablePath)) {
|
|
247
264
|
if (fs.existsSync(singleErrorDistDir)) {
|
|
@@ -273,6 +290,8 @@ test('single command extension shows error when command throws at root scope', a
|
|
|
273
290
|
await session.waitIdle()
|
|
274
291
|
|
|
275
292
|
const errorSnapshot = await session.text()
|
|
293
|
+
expect(errorSnapshot).not.toContain('Failed to load native binding')
|
|
294
|
+
expect(errorSnapshot).not.toContain('@swc/core/binding.js')
|
|
276
295
|
expect(errorSnapshot).toMatchInlineSnapshot(`
|
|
277
296
|
"
|
|
278
297
|
|
|
@@ -297,4 +316,3 @@ test('single command extension shows error when command throws at root scope', a
|
|
|
297
316
|
"
|
|
298
317
|
`)
|
|
299
318
|
}, 60000)
|
|
300
|
-
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, {
|
|
2
2
|
ReactNode,
|
|
3
3
|
useState,
|
|
4
|
-
|
|
4
|
+
useLayoutEffect,
|
|
5
5
|
useMemo,
|
|
6
6
|
useRef,
|
|
7
7
|
useCallback,
|
|
@@ -178,8 +178,8 @@ const Dropdown: DropdownType = (props) => {
|
|
|
178
178
|
[searchText, filtering, selected, currentValue, onSelectionChange],
|
|
179
179
|
)
|
|
180
180
|
|
|
181
|
-
// Update controlled value
|
|
182
|
-
|
|
181
|
+
// Update controlled value (before paint to avoid flash)
|
|
182
|
+
useLayoutEffect(() => {
|
|
183
183
|
if (value !== undefined) {
|
|
184
184
|
setCurrentValue(value)
|
|
185
185
|
}
|
|
@@ -241,9 +241,11 @@ export function Footer({
|
|
|
241
241
|
<ToastInline toast={toast} />
|
|
242
242
|
) : (
|
|
243
243
|
<>
|
|
244
|
-
{
|
|
244
|
+
<box flexDirection='row' overflow='hidden' height={1} flexShrink={1}>
|
|
245
|
+
{children}
|
|
246
|
+
</box>
|
|
245
247
|
{showPoweredBy && (
|
|
246
|
-
<box flexDirection='row' gap={1}>
|
|
248
|
+
<box flexDirection='row' gap={1} flexShrink={0}>
|
|
247
249
|
<text flexShrink={0} fg={theme.textMuted}>
|
|
248
250
|
powered by
|
|
249
251
|
</text>
|