forked from CoreBunch/Instatic
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdev.ts
More file actions
291 lines (260 loc) · 8 KB
/
Copy pathdev.ts
File metadata and controls
291 lines (260 loc) · 8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
/**
* One-command dev server.
*
* `bun run dev` is the only thing a developer should need.
*
* Default behaviour (no DATABASE_URL in the environment): the script uses
* SQLite at ./.tmp/dev.db — no Docker or any other external services required.
* The parent directory is created automatically on first run.
*
* Postgres mode: set DATABASE_URL=postgres://... to run against a Postgres
* database. The script will manage a local docker postgres for you:
*
* 1. Verifies the docker daemon is reachable.
* 2. Starts the `postgres` compose service if it isn't running.
* 3. Stops the `app` compose service if it IS running (it would
* otherwise hold port 3001 and block the local cms).
* 4. Waits until postgres actually accepts connections.
*
* Either way, the script then:
*
* - Pre-checks ports 3001 (cms) and 5173 (vite) and prints an
* actionable message if either is held by something we don't own.
* - Spawns the cms (`bun --watch server/index.ts`) and vite
* (`vite --host 127.0.0.1`) as children, forwarding their output
* and signals so Ctrl+C cleanly kills both.
*/
import { mkdir } from 'node:fs/promises'
import { dirname } from 'node:path'
import { isSqliteUrl } from '../server/db'
import { ensurePortFree } from './lib/freePort'
const CMS_PORT = Number(process.env.PORT ?? '3001')
const VITE_PORT = Number(process.env.VITE_PORT ?? '5173')
const POSTGRES_HOST = '127.0.0.1'
const POSTGRES_PORT = 5433
const DATABASE_URL = process.env.DATABASE_URL ?? 'sqlite:./.tmp/dev.db'
const decoder = new TextDecoder()
function log(msg: string): void {
console.error(`[dev] ${msg}`)
}
function fail(msg: string): never {
log(msg)
process.exit(1)
}
// --- docker helpers -------------------------------------------------------
function dockerInstalled(): boolean {
const result = Bun.spawnSync(['docker', '--version'], {
stdout: 'ignore',
stderr: 'ignore',
})
return result.exitCode === 0
}
function dockerDaemonRunning(): boolean {
const result = Bun.spawnSync(['docker', 'info'], {
stdout: 'ignore',
stderr: 'ignore',
})
return result.exitCode === 0
}
interface ComposeServiceState {
state: 'running' | 'exited' | 'paused' | 'created' | 'restarting' | 'dead' | 'absent'
}
/**
* Reads the state of a compose service. Handles both the NDJSON output
* from older docker-compose versions and the JSON-array output from newer
* versions; returns 'absent' when no entry is found.
*/
function getComposeServiceState(service: string): ComposeServiceState['state'] {
const result = Bun.spawnSync(
['docker', 'compose', 'ps', '--all', '--format', 'json', service],
{ stdout: 'pipe', stderr: 'pipe' },
)
if (result.exitCode !== 0) return 'absent'
const stdout = decoder.decode(result.stdout).trim()
if (!stdout) return 'absent'
const parseEntry = (raw: unknown): ComposeServiceState['state'] | null => {
if (!raw || typeof raw !== 'object') return null
const entry = raw as { Service?: string; State?: string; Name?: string }
if (entry.Service !== service) return null
const state = entry.State?.toLowerCase()
if (
state === 'running' ||
state === 'exited' ||
state === 'paused' ||
state === 'created' ||
state === 'restarting' ||
state === 'dead'
) {
return state
}
return null
}
// Newer compose: a single JSON array.
if (stdout.startsWith('[')) {
try {
const arr = JSON.parse(stdout) as unknown[]
for (const entry of arr) {
const state = parseEntry(entry)
if (state) return state
}
} catch {
// fall through to NDJSON
}
}
// Older / default compose: one JSON object per line.
for (const line of stdout.split('\n')) {
const trimmed = line.trim()
if (!trimmed) continue
try {
const state = parseEntry(JSON.parse(trimmed))
if (state) return state
} catch {
// ignore unparseable lines
}
}
return 'absent'
}
function runDocker(args: string[], description: string): void {
log(description)
const result = Bun.spawnSync(['docker', ...args], {
stdout: 'inherit',
stderr: 'inherit',
})
if (result.exitCode !== 0) {
fail(`\`docker ${args.join(' ')}\` exited with code ${result.exitCode}.`)
}
}
function ensurePostgresRunning(): void {
const state = getComposeServiceState('postgres')
switch (state) {
case 'running':
log('Docker postgres is already running.')
return
case 'exited':
case 'created':
case 'paused':
case 'restarting':
case 'dead':
runDocker(
['compose', 'start', 'postgres'],
`Docker postgres is ${state} — starting it...`,
)
return
case 'absent':
runDocker(
['compose', 'up', '-d', 'postgres'],
'Docker postgres container not found — creating it...',
)
return
}
}
function stopAppContainerIfRunning(): void {
const state = getComposeServiceState('app')
if (state === 'running') {
runDocker(
['compose', 'stop', 'app'],
'Docker `app` container is running — stopping it (it would conflict with the local cms on port 3001)...',
)
}
}
async function waitForPostgresReady(timeoutMs = 60_000): Promise<void> {
log(`Waiting for postgres at ${POSTGRES_HOST}:${POSTGRES_PORT}...`)
const start = Date.now()
while (Date.now() - start < timeoutMs) {
const result = Bun.spawnSync(
[
'docker',
'compose',
'exec',
'-T',
'postgres',
'pg_isready',
'-U',
'instatic',
'-d',
'instatic',
],
{ stdout: 'ignore', stderr: 'ignore' },
)
if (result.exitCode === 0) {
log('Postgres is accepting connections.')
return
}
await new Promise((resolve) => setTimeout(resolve, 500))
}
fail(`Postgres did not become ready within ${timeoutMs}ms.`)
}
// --- main -----------------------------------------------------------------
if (isSqliteUrl(DATABASE_URL)) {
const dbPath = DATABASE_URL.replace(/^sqlite:|^file:/, '')
await mkdir(dirname(dbPath), { recursive: true })
log(`Using SQLite at ${dbPath} — skipping Postgres docker provisioning`)
} else {
if (!dockerInstalled()) {
fail('Docker is not installed. Install Docker Desktop, or set DATABASE_URL to point at your own postgres.')
}
if (!dockerDaemonRunning()) {
fail('Docker daemon is not running. Start Docker Desktop, or set DATABASE_URL to point at your own postgres.')
}
ensurePostgresRunning()
stopAppContainerIfRunning()
await waitForPostgresReady()
}
await ensurePortFree(CMS_PORT, 'cms', log)
await ensurePortFree(VITE_PORT, 'vite', log)
log('')
log(`Open the editor at: http://localhost:${VITE_PORT}`)
log(`CMS API runs on: http://localhost:${CMS_PORT} (you usually don't open this directly)`)
log('')
// --- spawn cms + vite -----------------------------------------------------
interface DevProcess {
name: string
command: string
env?: Record<string, string>
}
const processes: DevProcess[] = [
{
name: 'cms',
command: 'bun --watch server/index.ts',
env: {
PORT: String(CMS_PORT),
DATABASE_URL,
STATIC_DIR: process.env.STATIC_DIR ?? './dist',
UPLOADS_DIR: process.env.UPLOADS_DIR ?? './uploads',
},
},
{
name: 'vite',
command: `vite --host 127.0.0.1 --port ${VITE_PORT} --strictPort`,
},
]
const children: Bun.Subprocess[] = []
let shuttingDown = false
function stopChildren(signal: NodeJS.Signals = 'SIGTERM'): void {
for (const child of children) {
if (child.exitCode === null) child.kill(signal)
}
}
for (const cfg of processes) {
const child = Bun.spawn(cfg.command.split(' '), {
env: { ...process.env, ...cfg.env },
stdin: 'inherit',
stdout: 'inherit',
stderr: 'inherit',
})
children.push(child)
void child.exited.then((code) => {
if (shuttingDown) return
shuttingDown = true
stopChildren()
process.exit(code)
})
}
process.on('SIGINT', () => {
shuttingDown = true
stopChildren('SIGINT')
})
process.on('SIGTERM', () => {
shuttingDown = true
stopChildren('SIGTERM')
})