golem-kit 0.1.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 +41 -0
- package/assets/golem-mascot.png +0 -0
- package/docs/local-cli.md +64 -0
- package/docs/source-development.md +65 -0
- package/golem.config.ts +1 -0
- package/index.html +12 -0
- package/package.json +48 -0
- package/src/app.tsx +10 -0
- package/src/browser/adapters.ts +164 -0
- package/src/browser/app.d.ts +9 -0
- package/src/browser/app.tsx +63 -0
- package/src/browser/main.tsx +9 -0
- package/src/browser/vite-env.d.ts +1 -0
- package/src/browser-build.ts +70 -0
- package/src/cli.ts +127 -0
- package/src/dev-server.ts +190 -0
- package/src/runtime/codex.ts +119 -0
- package/src/runtime/discovery.ts +92 -0
- package/src/runtime/index.ts +2 -0
- package/src/runtime/session.ts +326 -0
- package/src/runtime/state.ts +42 -0
- package/tsconfig.json +16 -0
- package/vite.config.ts +76 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import type { AgentName } from './discovery.ts'
|
|
3
|
+
|
|
4
|
+
export type BackendEvent =
|
|
5
|
+
| { type: 'message'; text: string }
|
|
6
|
+
| { type: 'interrupted'; reason?: string }
|
|
7
|
+
| { type: 'error'; message: string }
|
|
8
|
+
|
|
9
|
+
/** The runtime contract only; Claude/Codex process bridging is intentionally not implemented yet. */
|
|
10
|
+
export type SessionBackend = {
|
|
11
|
+
start(emit: (event: BackendEvent) => void): Promise<void>
|
|
12
|
+
send(text: string): Promise<void>
|
|
13
|
+
shutdown(): Promise<void>
|
|
14
|
+
interrupt?(): Promise<void>
|
|
15
|
+
threadId?(): string | undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type SessionStatus = 'starting' | 'ready' | 'interrupted' | 'stopped' | 'failed'
|
|
19
|
+
|
|
20
|
+
export type SessionEvent = {
|
|
21
|
+
sequence: number
|
|
22
|
+
sessionId: string
|
|
23
|
+
type: 'status' | 'user' | 'message' | 'interrupted' | 'error' | 'rebuilt'
|
|
24
|
+
status?: SessionStatus
|
|
25
|
+
text?: string
|
|
26
|
+
reason?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class Session {
|
|
30
|
+
readonly history: SessionEvent[] = []
|
|
31
|
+
status: SessionStatus = 'starting'
|
|
32
|
+
private readonly listeners = new Set<(event: SessionEvent) => void>()
|
|
33
|
+
private sequence = 0
|
|
34
|
+
private readonly pending: Array<{ text: string; resolve: () => void; reject: (error: Error) => void }> = []
|
|
35
|
+
private active = false
|
|
36
|
+
private dispatchScheduled = false
|
|
37
|
+
private closed = false
|
|
38
|
+
private shutdownPromise: Promise<void> | undefined
|
|
39
|
+
private workerShutdownPromise: Promise<void> | undefined
|
|
40
|
+
private activeReject: ((error: Error) => void) | undefined
|
|
41
|
+
private workerStarted = false
|
|
42
|
+
private persistence = Promise.resolve()
|
|
43
|
+
private persistenceError: Error | undefined
|
|
44
|
+
readonly id: string
|
|
45
|
+
readonly backend: AgentName
|
|
46
|
+
/** Server-owned: set once at creation from the request's explicit build intent, never inferred. */
|
|
47
|
+
readonly buildMode: boolean
|
|
48
|
+
private readonly worker: SessionBackend
|
|
49
|
+
private readonly save?: (snapshot: SessionSnapshot) => Promise<void>
|
|
50
|
+
|
|
51
|
+
constructor(
|
|
52
|
+
backend: AgentName,
|
|
53
|
+
worker: SessionBackend,
|
|
54
|
+
id: string,
|
|
55
|
+
buildMode = false,
|
|
56
|
+
save?: (snapshot: SessionSnapshot) => Promise<void>,
|
|
57
|
+
) {
|
|
58
|
+
this.id = id
|
|
59
|
+
this.backend = backend
|
|
60
|
+
this.worker = worker
|
|
61
|
+
this.buildMode = buildMode
|
|
62
|
+
this.save = save
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async start(): Promise<void> {
|
|
66
|
+
try {
|
|
67
|
+
await this.startWorker()
|
|
68
|
+
if (this.status !== 'starting') {
|
|
69
|
+
await this.closeWorker()
|
|
70
|
+
throw new Error(`Session ${this.status} during startup`)
|
|
71
|
+
}
|
|
72
|
+
this.setStatus('ready')
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (this.status === 'starting') {
|
|
75
|
+
this.setStatus('failed')
|
|
76
|
+
this.record({ type: 'error', text: error instanceof Error ? error.message : String(error) })
|
|
77
|
+
}
|
|
78
|
+
await this.closeWorker()
|
|
79
|
+
throw error
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static restore(snapshot: SessionSnapshot, worker: SessionBackend, save?: (snapshot: SessionSnapshot) => Promise<void>): Session {
|
|
84
|
+
const session = new Session(snapshot.backend, worker, snapshot.id, snapshot.buildMode, save)
|
|
85
|
+
session.history.push(...snapshot.history)
|
|
86
|
+
session.status = snapshot.status
|
|
87
|
+
session.sequence = Math.max(-1, ...snapshot.history.map((event) => event.sequence)) + 1
|
|
88
|
+
session.closed = snapshot.status === 'stopped'
|
|
89
|
+
if (snapshot.active) session.recoverInterrupted()
|
|
90
|
+
return session
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
snapshot(): SessionSnapshot {
|
|
94
|
+
return { id: this.id, backend: this.backend, buildMode: this.buildMode, status: this.status, active: this.active, history: this.history, threadId: this.worker.threadId?.() }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async flush(): Promise<void> {
|
|
98
|
+
await this.persistence
|
|
99
|
+
if (this.persistenceError) throw this.persistenceError
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
send(text: string): Promise<void> {
|
|
103
|
+
if (this.closed || (this.status !== 'ready' && this.status !== 'interrupted' && this.status !== 'failed')) {
|
|
104
|
+
return Promise.reject(new Error(`Session ${this.status}`))
|
|
105
|
+
}
|
|
106
|
+
if (this.status === 'interrupted' || this.status === 'failed') this.setStatus('ready')
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
this.pending.push({ text, resolve, reject })
|
|
109
|
+
this.pump()
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
subscribe(listener: (event: SessionEvent) => void): () => void {
|
|
114
|
+
this.listeners.add(listener)
|
|
115
|
+
return () => this.listeners.delete(listener)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
subscribeFrom(sequence: number, listener: (event: SessionEvent) => void): () => void {
|
|
119
|
+
for (const event of this.history) if (event.sequence > sequence) listener(event)
|
|
120
|
+
return this.subscribe(listener)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async shutdown(): Promise<void> {
|
|
124
|
+
if (this.shutdownPromise) return this.shutdownPromise
|
|
125
|
+
this.closed = true
|
|
126
|
+
this.setStatus('stopped')
|
|
127
|
+
this.rejectPending(new Error('Session stopped'))
|
|
128
|
+
this.shutdownPromise = this.closeWorker()
|
|
129
|
+
return this.shutdownPromise
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async dispose(): Promise<void> { await this.closeWorker() }
|
|
133
|
+
|
|
134
|
+
abandon(): void {
|
|
135
|
+
if (!this.active || this.closed) return
|
|
136
|
+
this.setStatus('interrupted')
|
|
137
|
+
this.record({ type: 'interrupted', reason: 'server restarted during this turn' })
|
|
138
|
+
this.activeReject?.(new Error('Session interrupted'))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async interrupt(): Promise<void> {
|
|
142
|
+
if (this.closed || this.status === 'stopped') return
|
|
143
|
+
if (this.status !== 'ready') return
|
|
144
|
+
this.setStatus('interrupted')
|
|
145
|
+
this.record({ type: 'interrupted', reason: 'interrupted by user' })
|
|
146
|
+
this.activeReject?.(new Error('Session interrupted'))
|
|
147
|
+
this.rejectPending(new Error('Session interrupted'))
|
|
148
|
+
await this.worker.interrupt?.()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Server-owned rebuild-then-refresh signal; fired once after a successful build-mode turn. */
|
|
152
|
+
notifyRebuilt(): void {
|
|
153
|
+
if (this.closed) return
|
|
154
|
+
this.record({ type: 'rebuilt' })
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Routed through the existing error surface: visible in chat, session stays recoverable. */
|
|
158
|
+
notifyBuildFailed(message: string): void {
|
|
159
|
+
if (this.closed) return
|
|
160
|
+
this.setStatus('failed')
|
|
161
|
+
this.record({ type: 'error', text: message })
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private receive(event: BackendEvent): void {
|
|
165
|
+
if (this.closed) return
|
|
166
|
+
if (event.type === 'message') this.record({ type: 'message', text: event.text })
|
|
167
|
+
if (event.type === 'interrupted') {
|
|
168
|
+
const alreadyInterrupted = this.status === 'interrupted'
|
|
169
|
+
if (!alreadyInterrupted) this.setStatus('interrupted')
|
|
170
|
+
this.rejectPending(new Error('Session interrupted'))
|
|
171
|
+
if (!alreadyInterrupted) this.record({ type: 'interrupted', reason: event.reason })
|
|
172
|
+
}
|
|
173
|
+
if (event.type === 'error') {
|
|
174
|
+
this.setStatus('failed')
|
|
175
|
+
this.rejectPending(new Error('Session failed'))
|
|
176
|
+
this.record({ type: 'error', text: event.message })
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private pump(): void {
|
|
181
|
+
if (this.active || this.dispatchScheduled || this.closed || this.status !== 'ready') return
|
|
182
|
+
const next = this.pending[0]
|
|
183
|
+
if (!next) return
|
|
184
|
+
this.dispatchScheduled = true
|
|
185
|
+
Promise.resolve()
|
|
186
|
+
.then(async () => {
|
|
187
|
+
this.dispatchScheduled = false
|
|
188
|
+
if (this.closed || this.status !== 'ready' || this.pending[0] !== next) {
|
|
189
|
+
this.pump()
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
this.pending.shift()
|
|
193
|
+
this.active = true
|
|
194
|
+
this.record({ type: 'user', text: next.text })
|
|
195
|
+
if (this.closed || this.status !== 'ready') {
|
|
196
|
+
this.active = false
|
|
197
|
+
next.reject(new Error(`Session ${this.status}`))
|
|
198
|
+
this.pump()
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
await this.startWorker()
|
|
203
|
+
if (this.closed || this.status !== 'ready') {
|
|
204
|
+
this.active = false
|
|
205
|
+
next.reject(new Error(`Session ${this.status}`))
|
|
206
|
+
this.pump()
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
this.activeReject = next.reject
|
|
210
|
+
Promise.resolve(this.worker.send(next.text)).then(next.resolve, next.reject).finally(() => {
|
|
211
|
+
this.activeReject = undefined
|
|
212
|
+
this.active = false
|
|
213
|
+
this.persist()
|
|
214
|
+
this.pump()
|
|
215
|
+
})
|
|
216
|
+
} catch (error) {
|
|
217
|
+
this.active = false
|
|
218
|
+
next.reject(error instanceof Error ? error : new Error(String(error)))
|
|
219
|
+
this.pump()
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private rejectPending(error: Error): void {
|
|
225
|
+
while (this.pending.length) this.pending.shift()?.reject(error)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private closeWorker(): Promise<void> {
|
|
229
|
+
if (!this.workerShutdownPromise) this.workerShutdownPromise = Promise.resolve().then(() => this.worker.shutdown())
|
|
230
|
+
return this.workerShutdownPromise
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private async startWorker(): Promise<boolean> {
|
|
234
|
+
if (this.workerStarted) return false
|
|
235
|
+
await this.worker.start((event) => this.receive(event))
|
|
236
|
+
this.workerStarted = true
|
|
237
|
+
return true
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private recoverInterrupted(): void {
|
|
241
|
+
this.active = false
|
|
242
|
+
this.status = 'interrupted'
|
|
243
|
+
this.record({ type: 'status', status: 'interrupted' })
|
|
244
|
+
this.record({ type: 'interrupted', reason: 'server restarted during this turn' })
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private setStatus(status: SessionStatus): void {
|
|
248
|
+
this.status = status
|
|
249
|
+
this.record({ type: 'status', status })
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private record(event: Omit<SessionEvent, 'sequence' | 'sessionId'>): void {
|
|
253
|
+
const complete = { ...event, sequence: this.sequence++, sessionId: this.id }
|
|
254
|
+
this.history.push(complete)
|
|
255
|
+
this.listeners.forEach((listener) => listener(complete))
|
|
256
|
+
this.persist()
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private persist(): void {
|
|
260
|
+
if (!this.save || this.persistenceError) return
|
|
261
|
+
const snapshot = this.snapshot()
|
|
262
|
+
this.persistence = this.persistence.catch(() => {}).then(() => this.save!(snapshot))
|
|
263
|
+
void this.persistence.catch((error) => {
|
|
264
|
+
if (this.persistenceError) return
|
|
265
|
+
this.persistenceError = error instanceof Error ? error : new Error(String(error))
|
|
266
|
+
const complete = { type: 'error' as const, text: `Unable to save conversation: ${this.persistenceError.message}`, sequence: this.sequence++, sessionId: this.id }
|
|
267
|
+
this.history.push(complete)
|
|
268
|
+
this.listeners.forEach((listener) => listener(complete))
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export type SessionSnapshot = {
|
|
274
|
+
id: string
|
|
275
|
+
backend: AgentName
|
|
276
|
+
buildMode: boolean
|
|
277
|
+
status: SessionStatus
|
|
278
|
+
active: boolean
|
|
279
|
+
history: SessionEvent[]
|
|
280
|
+
threadId?: string
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export class SessionManager {
|
|
284
|
+
private readonly sessions = new Map<string, Session>()
|
|
285
|
+
private readonly save: ((snapshots: SessionSnapshot[]) => Promise<void>) | undefined
|
|
286
|
+
|
|
287
|
+
constructor(save?: (snapshots: SessionSnapshot[]) => Promise<void>) { this.save = save }
|
|
288
|
+
|
|
289
|
+
private persist = async (): Promise<void> => this.save?.([...this.sessions.values()].map((session) => session.snapshot()))
|
|
290
|
+
|
|
291
|
+
async start(backend: AgentName, worker: SessionBackend, buildMode = false): Promise<Session> {
|
|
292
|
+
const session = new Session(backend, worker, randomUUID(), buildMode, this.persist)
|
|
293
|
+
this.sessions.set(session.id, session)
|
|
294
|
+
try {
|
|
295
|
+
await session.start()
|
|
296
|
+
await session.flush()
|
|
297
|
+
return session
|
|
298
|
+
} catch (error) {
|
|
299
|
+
this.sessions.delete(session.id)
|
|
300
|
+
throw error
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
get(id: string): Session | undefined { return this.sessions.get(id) }
|
|
305
|
+
|
|
306
|
+
restore(snapshots: SessionSnapshot[], createWorker: (snapshot: SessionSnapshot) => SessionBackend): void {
|
|
307
|
+
for (const snapshot of snapshots) this.sessions.set(snapshot.id, Session.restore(snapshot, createWorker(snapshot), this.persist))
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async shutdownAll(): Promise<void> {
|
|
311
|
+
await Promise.all([...this.sessions.values()].map((session) => session.shutdown()))
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async disposeAll(): Promise<void> {
|
|
315
|
+
for (const session of this.sessions.values()) session.abandon()
|
|
316
|
+
await Promise.all([...this.sessions.values()].map((session) => session.dispose()))
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async flushAll(): Promise<void> { await Promise.all([...this.sessions.values()].map((session) => session.flush())) }
|
|
320
|
+
|
|
321
|
+
async shutdown(id: string): Promise<void> {
|
|
322
|
+
const session = this.sessions.get(id)
|
|
323
|
+
if (!session) throw new Error(`Unknown session ${id}`)
|
|
324
|
+
await session.shutdown()
|
|
325
|
+
}
|
|
326
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
import { randomUUID } from 'node:crypto'
|
|
4
|
+
import type { SessionSnapshot } from './session.ts'
|
|
5
|
+
|
|
6
|
+
type State = { version: 1; sessions: SessionSnapshot[] }
|
|
7
|
+
|
|
8
|
+
export class ConversationState {
|
|
9
|
+
private readonly file: string
|
|
10
|
+
private writes = Promise.resolve()
|
|
11
|
+
|
|
12
|
+
constructor(directory: string) { this.file = join(directory, 'conversations.json') }
|
|
13
|
+
|
|
14
|
+
async load(): Promise<SessionSnapshot[]> {
|
|
15
|
+
try {
|
|
16
|
+
const state = JSON.parse(await readFile(this.file, 'utf8')) as State
|
|
17
|
+
if (state.version !== 1 || !Array.isArray(state.sessions)) throw new Error('unsupported conversation state')
|
|
18
|
+
return state.sessions
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []
|
|
21
|
+
throw new Error(`Cannot load saved conversations: ${error instanceof Error ? error.message : String(error)}`)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async save(sessions: SessionSnapshot[]): Promise<void> {
|
|
26
|
+
const contents = JSON.stringify({ version: 1, sessions }) + '\n'
|
|
27
|
+
this.writes = this.writes.catch(() => {}).then(() => this.replace(contents))
|
|
28
|
+
return this.writes
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private async replace(contents: string): Promise<void> {
|
|
32
|
+
const temporary = `${this.file}.${randomUUID()}.tmp`
|
|
33
|
+
try {
|
|
34
|
+
await mkdir(dirname(this.file), { recursive: true })
|
|
35
|
+
await writeFile(temporary, contents, 'utf8')
|
|
36
|
+
await rename(temporary, this.file)
|
|
37
|
+
} catch (error) {
|
|
38
|
+
try { await import('node:fs/promises').then(({ unlink }) => unlink(temporary)) } catch {}
|
|
39
|
+
throw new Error(`Cannot save conversations: ${error instanceof Error ? error.message : String(error)}`)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2023",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"allowImportingTsExtensions": true,
|
|
9
|
+
"jsx": "react-jsx",
|
|
10
|
+
"erasableSyntaxOnly": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"types": ["node"],
|
|
13
|
+
"paths": { "@golem/app": ["./src/app.tsx"], "@golem/config": ["./golem.config.ts"] }
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts"]
|
|
16
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
import { defineConfig, type UserConfig } from 'vite'
|
|
6
|
+
import react from '@vitejs/plugin-react'
|
|
7
|
+
|
|
8
|
+
const frameworkRoot = import.meta.dirname
|
|
9
|
+
|
|
10
|
+
export type UiSource = {
|
|
11
|
+
root: string
|
|
12
|
+
revision: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function gitRevision(root: string): string {
|
|
16
|
+
try {
|
|
17
|
+
return execFileSync('git', ['-C', root, 'rev-parse', '--short', 'HEAD'], { encoding: 'utf8' }).trim()
|
|
18
|
+
} catch {
|
|
19
|
+
return 'unavailable'
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function resolveUiSource(): UiSource | undefined {
|
|
24
|
+
const configured = process.env.GOLEM_UI_SOURCE
|
|
25
|
+
if (!configured) return undefined
|
|
26
|
+
|
|
27
|
+
const root = resolve(configured)
|
|
28
|
+
if (!existsSync(resolve(root, 'package.json'))) {
|
|
29
|
+
throw new Error(`GOLEM_UI_SOURCE must point to a golem-ui checkout containing package.json: ${root}`)
|
|
30
|
+
}
|
|
31
|
+
if (!existsSync(resolve(root, 'src/index.ts')) || !existsSync(resolve(root, 'src/styles.css'))) {
|
|
32
|
+
throw new Error(`GOLEM_UI_SOURCE is missing golem-ui src/index.ts or src/styles.css: ${root}`)
|
|
33
|
+
}
|
|
34
|
+
return { root, revision: gitRevision(root) }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default defineConfig(async (): Promise<UserConfig> => {
|
|
38
|
+
const ui = resolveUiSource()
|
|
39
|
+
const appRoot = resolve(process.cwd())
|
|
40
|
+
const plugins: NonNullable<UserConfig['plugins']> = [react()]
|
|
41
|
+
if (ui) {
|
|
42
|
+
const styles = resolve(ui.root, 'src/styles.css')
|
|
43
|
+
plugins.push({
|
|
44
|
+
name: 'golem-ui-source-path',
|
|
45
|
+
enforce: 'pre',
|
|
46
|
+
transform(code: string, id: string) {
|
|
47
|
+
const sourcePath = JSON.stringify(ui.root.replaceAll('\\', '/'))
|
|
48
|
+
return id.split('?')[0] === styles
|
|
49
|
+
? { code: `@source ${sourcePath};\n${code}`, map: null }
|
|
50
|
+
: undefined
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
const tailwindPlugin = resolve(ui.root, 'node_modules/@tailwindcss/vite/dist/index.mjs')
|
|
54
|
+
if (!existsSync(tailwindPlugin)) {
|
|
55
|
+
throw new Error(`GOLEM_UI_SOURCE needs @tailwindcss/vite installed; run pnpm install in ${ui.root}`)
|
|
56
|
+
}
|
|
57
|
+
const { default: tailwindcss } = await import(pathToFileURL(tailwindPlugin).href)
|
|
58
|
+
plugins.push(tailwindcss())
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
plugins,
|
|
62
|
+
resolve: {
|
|
63
|
+
alias: [
|
|
64
|
+
...(ui ? [
|
|
65
|
+
{ find: /^golem-ui$/, replacement: resolve(ui.root, 'src/index.ts') },
|
|
66
|
+
{ find: /^golem-ui\/styles\.css$/, replacement: resolve(ui.root, 'src/styles.css') },
|
|
67
|
+
] : []),
|
|
68
|
+
{ find: /^@golem\/app$/, replacement: resolve(appRoot, 'src/app.tsx') },
|
|
69
|
+
{ find: /^@golem\/config$/, replacement: resolve(appRoot, 'golem.config.ts') },
|
|
70
|
+
],
|
|
71
|
+
dedupe: ['react', 'react-dom'],
|
|
72
|
+
},
|
|
73
|
+
root: frameworkRoot,
|
|
74
|
+
build: { outDir: resolve(appRoot, 'dist'), emptyOutDir: true },
|
|
75
|
+
}
|
|
76
|
+
})
|