@livestore/utils-dev 0.4.0 → 0.5.0-dev.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/dist/.tsbuildinfo +1 -1
- package/dist/node/DockerCompose/DockerCompose.d.ts +52 -0
- package/dist/node/DockerCompose/DockerCompose.d.ts.map +1 -0
- package/dist/node/DockerCompose/DockerCompose.js +158 -0
- package/dist/node/DockerCompose/DockerCompose.js.map +1 -0
- package/dist/node/DockerCompose/DockerCompose.test.d.ts +2 -0
- package/dist/node/DockerCompose/DockerCompose.test.d.ts.map +1 -0
- package/dist/node/{DockerComposeService/DockerComposeService.test.js → DockerCompose/DockerCompose.test.js} +16 -16
- package/dist/node/DockerCompose/DockerCompose.test.js.map +1 -0
- package/dist/node/DockerCompose/mod.d.ts +2 -0
- package/dist/node/DockerCompose/mod.d.ts.map +1 -0
- package/dist/node/DockerCompose/mod.js +2 -0
- package/dist/node/DockerCompose/mod.js.map +1 -0
- package/dist/node/FileLogger.d.ts +15 -8
- package/dist/node/FileLogger.d.ts.map +1 -1
- package/dist/node/FileLogger.js +63 -79
- package/dist/node/FileLogger.js.map +1 -1
- package/dist/node/cmd-log.d.ts.map +1 -1
- package/dist/node/cmd-log.js +19 -8
- package/dist/node/cmd-log.js.map +1 -1
- package/dist/node/cmd.d.ts +10 -12
- package/dist/node/cmd.d.ts.map +1 -1
- package/dist/node/cmd.js +73 -36
- package/dist/node/cmd.js.map +1 -1
- package/dist/node/cmd.test.js +17 -17
- package/dist/node/cmd.test.js.map +1 -1
- package/dist/node/mod.d.ts +4 -5
- package/dist/node/mod.d.ts.map +1 -1
- package/dist/node/mod.js +58 -55
- package/dist/node/mod.js.map +1 -1
- package/dist/node/workspace.d.ts +2 -2
- package/dist/node/workspace.d.ts.map +1 -1
- package/dist/node/workspace.js +7 -7
- package/dist/node/workspace.js.map +1 -1
- package/dist/node-vitest/Vitest.d.ts +17 -8
- package/dist/node-vitest/Vitest.d.ts.map +1 -1
- package/dist/node-vitest/Vitest.js +5 -5
- package/dist/node-vitest/Vitest.js.map +1 -1
- package/dist/node-vitest/Vitest.test.d.ts +3 -5
- package/dist/node-vitest/Vitest.test.d.ts.map +1 -1
- package/dist/node-vitest/Vitest.test.js +14 -3
- package/dist/node-vitest/Vitest.test.js.map +1 -1
- package/dist/wrangler/WranglerDevServer.d.ts +15 -20
- package/dist/wrangler/WranglerDevServer.d.ts.map +1 -1
- package/dist/wrangler/WranglerDevServer.js +77 -71
- package/dist/wrangler/WranglerDevServer.js.map +1 -1
- package/dist/wrangler/WranglerDevServer.test.js +28 -28
- package/dist/wrangler/WranglerDevServer.test.js.map +1 -1
- package/dist/wrangler/mod.d.ts +1 -1
- package/dist/wrangler/mod.d.ts.map +1 -1
- package/dist/wrangler/mod.js +1 -1
- package/dist/wrangler/mod.js.map +1 -1
- package/package.json +19 -37
- package/src/node/{DockerComposeService/DockerComposeService.test.ts → DockerCompose/DockerCompose.test.ts} +18 -18
- package/src/node/DockerCompose/DockerCompose.ts +328 -0
- package/src/node/DockerCompose/mod.ts +1 -0
- package/src/node/FileLogger.ts +89 -114
- package/src/node/cmd-log.ts +21 -10
- package/src/node/cmd.test.ts +19 -17
- package/src/node/cmd.ts +117 -86
- package/src/node/mod.ts +112 -104
- package/src/node/workspace.ts +9 -10
- package/src/node-vitest/Vitest.test.ts +22 -3
- package/src/node-vitest/Vitest.ts +32 -42
- package/src/wrangler/WranglerDevServer.test.ts +33 -37
- package/src/wrangler/WranglerDevServer.ts +145 -129
- package/src/wrangler/mod.ts +1 -6
- package/dist/node/DockerComposeService/DockerComposeService.d.ts +0 -63
- package/dist/node/DockerComposeService/DockerComposeService.d.ts.map +0 -1
- package/dist/node/DockerComposeService/DockerComposeService.js +0 -149
- package/dist/node/DockerComposeService/DockerComposeService.js.map +0 -1
- package/dist/node/DockerComposeService/DockerComposeService.test.d.ts +0 -2
- package/dist/node/DockerComposeService/DockerComposeService.test.d.ts.map +0 -1
- package/dist/node/DockerComposeService/DockerComposeService.test.js.map +0 -1
- package/src/node/DockerComposeService/DockerComposeService.ts +0 -343
- /package/src/node/{DockerComposeService → DockerCompose}/test-fixtures/docker-compose.yml +0 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { objectToString, omitUndefineds } from '@livestore/utils'
|
|
2
|
+
import {
|
|
3
|
+
ChildProcess,
|
|
4
|
+
ChildProcessSpawner,
|
|
5
|
+
Context,
|
|
6
|
+
Duration,
|
|
7
|
+
Effect,
|
|
8
|
+
Fiber,
|
|
9
|
+
Layer,
|
|
10
|
+
type PlatformError,
|
|
11
|
+
Schedule,
|
|
12
|
+
Schema,
|
|
13
|
+
type Scope,
|
|
14
|
+
Stream,
|
|
15
|
+
} from '@livestore/utils/effect'
|
|
16
|
+
|
|
17
|
+
export class DockerComposeError extends Schema.TaggedError<DockerComposeError>(
|
|
18
|
+
'~@livestore/utils-dev/DockerComposeError',
|
|
19
|
+
)('DockerComposeError', {
|
|
20
|
+
cause: Schema.Defect(),
|
|
21
|
+
note: Schema.String,
|
|
22
|
+
/** Compose env of the failed invocation, if any — structured instead of string-formatted into `note`. */
|
|
23
|
+
env: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
24
|
+
}) {}
|
|
25
|
+
|
|
26
|
+
export interface Options {
|
|
27
|
+
readonly cwd: string
|
|
28
|
+
readonly serviceName?: string
|
|
29
|
+
/** Unique project name to isolate this compose instance. If not provided, a random one is generated. */
|
|
30
|
+
readonly projectName?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface StartOptions {
|
|
34
|
+
readonly detached?: boolean
|
|
35
|
+
readonly env?: Record<string, string>
|
|
36
|
+
readonly healthCheck?: {
|
|
37
|
+
readonly url: string
|
|
38
|
+
readonly timeout?: Duration.Duration
|
|
39
|
+
readonly interval?: Duration.Duration
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface LogsOptions {
|
|
44
|
+
readonly follow?: boolean
|
|
45
|
+
readonly tail?: number
|
|
46
|
+
readonly since?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface Service {
|
|
50
|
+
readonly pull: Effect.Effect<void, DockerComposeError | PlatformError.PlatformError>
|
|
51
|
+
readonly start: (
|
|
52
|
+
options?: StartOptions,
|
|
53
|
+
) => Effect.Effect<void, DockerComposeError | PlatformError.PlatformError, Scope.Scope>
|
|
54
|
+
readonly stop: Effect.Effect<void, DockerComposeError | PlatformError.PlatformError>
|
|
55
|
+
readonly down: (options?: {
|
|
56
|
+
readonly env?: Record<string, string>
|
|
57
|
+
readonly volumes?: boolean
|
|
58
|
+
readonly removeOrphans?: boolean
|
|
59
|
+
}) => Effect.Effect<void, DockerComposeError | PlatformError.PlatformError>
|
|
60
|
+
readonly logs: (
|
|
61
|
+
options?: LogsOptions,
|
|
62
|
+
) => Stream.Stream<string, DockerComposeError | PlatformError.PlatformError, Scope.Scope>
|
|
63
|
+
/** The unique project name used to isolate this compose instance */
|
|
64
|
+
readonly projectName: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const generateProjectName = (): string => `ls-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
68
|
+
|
|
69
|
+
export class DockerCompose extends Context.Service<DockerCompose, Service>()('@livestore/utils-dev/DockerCompose') {}
|
|
70
|
+
|
|
71
|
+
export const make = (args: Options) =>
|
|
72
|
+
Effect.gen(function* () {
|
|
73
|
+
const { cwd, serviceName } = args
|
|
74
|
+
const projectName = args.projectName ?? generateProjectName()
|
|
75
|
+
|
|
76
|
+
const commandExecutorContext = yield* Effect.context<ChildProcessSpawner.ChildProcessSpawner>()
|
|
77
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
|
78
|
+
|
|
79
|
+
const baseComposeArgs = ['-p', projectName]
|
|
80
|
+
|
|
81
|
+
const composeCommand = (args: ReadonlyArray<string>, options: ChildProcess.CommandOptions = {}) =>
|
|
82
|
+
ChildProcess.make('docker', ['compose', ...baseComposeArgs, ...args], { ...options, cwd })
|
|
83
|
+
|
|
84
|
+
const envOptions = (
|
|
85
|
+
env: Record<string, string> | undefined,
|
|
86
|
+
): Pick<ChildProcess.CommandOptions, 'env' | 'extendEnv'> => (env === undefined ? {} : { env, extendEnv: true })
|
|
87
|
+
|
|
88
|
+
const pull = Effect.gen(function* () {
|
|
89
|
+
yield* Effect.log(`Pulling Docker Compose images in ${cwd}`)
|
|
90
|
+
|
|
91
|
+
const process = yield* spawner.spawn(composeCommand(['pull'], { stdout: 'pipe', stderr: 'pipe' }))
|
|
92
|
+
|
|
93
|
+
const stdoutFiber = yield* process.stdout.pipe(
|
|
94
|
+
Stream.decodeText({ encoding: 'utf8' }),
|
|
95
|
+
Stream.runFold(
|
|
96
|
+
() => '',
|
|
97
|
+
(acc, chunk) => acc + chunk,
|
|
98
|
+
),
|
|
99
|
+
// Drain subprocess output immediately so the child cannot block while we wait for its exit code.
|
|
100
|
+
Effect.forkChild,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const stderrFiber = yield* process.stderr.pipe(
|
|
104
|
+
Stream.decodeText({ encoding: 'utf8' }),
|
|
105
|
+
Stream.runFold(
|
|
106
|
+
() => '',
|
|
107
|
+
(acc, chunk) => acc + chunk,
|
|
108
|
+
),
|
|
109
|
+
// Drain subprocess output immediately so the child cannot block while we wait for its exit code.
|
|
110
|
+
Effect.forkChild,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
const exitCode = yield* process.exitCode
|
|
114
|
+
const stdout = yield* Fiber.join(stdoutFiber)
|
|
115
|
+
const stderr = yield* Fiber.join(stderrFiber)
|
|
116
|
+
|
|
117
|
+
const exitCodeNumber = Number(exitCode)
|
|
118
|
+
|
|
119
|
+
if (exitCodeNumber !== 0) {
|
|
120
|
+
const stdoutLog = stdout.length > 0 ? stdout : '<empty stdout>'
|
|
121
|
+
const stderrLog = stderr.length > 0 ? stderr : '<empty stderr>'
|
|
122
|
+
const failureMessage = [
|
|
123
|
+
`Docker compose pull failed with exit code ${exitCodeNumber} in ${cwd}`,
|
|
124
|
+
`stdout:\n${stdoutLog}`,
|
|
125
|
+
`stderr:\n${stderrLog}`,
|
|
126
|
+
].join('\n')
|
|
127
|
+
|
|
128
|
+
yield* Effect.logError(failureMessage)
|
|
129
|
+
|
|
130
|
+
return yield* new DockerComposeError({
|
|
131
|
+
cause: new Error(`Docker compose pull failed with exit code ${exitCodeNumber}`),
|
|
132
|
+
note: failureMessage,
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
yield* Effect.log(`Successfully pulled Docker Compose images`)
|
|
137
|
+
}).pipe(
|
|
138
|
+
Effect.retry({
|
|
139
|
+
schedule: Schedule.exponentialBackoff10Sec,
|
|
140
|
+
while: Schema.is(DockerComposeError),
|
|
141
|
+
}),
|
|
142
|
+
Effect.withSpan('pullDockerComposeImages'),
|
|
143
|
+
Effect.scoped,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
const start = Effect.fn('startDockerCompose')(function* (options: StartOptions = {}) {
|
|
147
|
+
const { detached = true, healthCheck } = options
|
|
148
|
+
|
|
149
|
+
const startArgs = ['up']
|
|
150
|
+
if (detached === true) startArgs.push('-d')
|
|
151
|
+
if (serviceName !== undefined) startArgs.push(serviceName)
|
|
152
|
+
|
|
153
|
+
const process = yield* spawner
|
|
154
|
+
.spawn(composeCommand(startArgs, { ...envOptions(options.env), stderr: 'inherit', stdout: 'inherit' }))
|
|
155
|
+
.pipe(
|
|
156
|
+
Effect.mapError(
|
|
157
|
+
(cause) =>
|
|
158
|
+
new DockerComposeError({
|
|
159
|
+
cause,
|
|
160
|
+
note: `Failed to start Docker Compose services in ${cwd}`,
|
|
161
|
+
}),
|
|
162
|
+
),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
const exitCode = yield* process.exitCode
|
|
166
|
+
|
|
167
|
+
if (Number(exitCode) !== 0) {
|
|
168
|
+
return yield* new DockerComposeError({
|
|
169
|
+
cause: new Error(`Docker compose exited with code ${exitCode}`),
|
|
170
|
+
note: `Docker Compose failed to start with exit code ${exitCode}`,
|
|
171
|
+
env: options.env,
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (healthCheck !== undefined) {
|
|
176
|
+
yield* performHealthCheck(healthCheck).pipe(Effect.provide(commandExecutorContext))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
yield* Effect.log(`Docker Compose services started successfully in ${cwd}`)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const stop = Effect.gen(function* () {
|
|
183
|
+
yield* Effect.log(`Stopping Docker Compose services in ${cwd}`)
|
|
184
|
+
|
|
185
|
+
const stopArgs = serviceName === undefined ? ['stop'] : ['stop', serviceName]
|
|
186
|
+
const exitCode = yield* spawner.exitCode(composeCommand(stopArgs))
|
|
187
|
+
|
|
188
|
+
if (Number(exitCode) !== 0) {
|
|
189
|
+
return yield* new DockerComposeError({
|
|
190
|
+
cause: new Error(`Docker compose stop exited with code ${exitCode}`),
|
|
191
|
+
note: `Failed to stop Docker Compose services`,
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
yield* Effect.log(`Docker Compose services stopped successfully`)
|
|
196
|
+
}).pipe(Effect.withSpan('stopDockerCompose'))
|
|
197
|
+
|
|
198
|
+
const logs = (options: LogsOptions = {}) =>
|
|
199
|
+
Effect.gen(function* () {
|
|
200
|
+
const { follow = false, tail, since } = options
|
|
201
|
+
|
|
202
|
+
const logsArgs = ['logs']
|
|
203
|
+
if (follow === true) logsArgs.push('-f')
|
|
204
|
+
if (tail !== undefined) logsArgs.push('--tail', tail.toString())
|
|
205
|
+
if (since !== undefined) logsArgs.push('--since', since)
|
|
206
|
+
if (serviceName !== undefined) logsArgs.push(serviceName)
|
|
207
|
+
|
|
208
|
+
const process = yield* spawner.spawn(composeCommand(logsArgs)).pipe(
|
|
209
|
+
Effect.mapError(
|
|
210
|
+
(cause) =>
|
|
211
|
+
new DockerComposeError({
|
|
212
|
+
cause,
|
|
213
|
+
note: `Failed to read Docker Compose logs in ${cwd}`,
|
|
214
|
+
}),
|
|
215
|
+
),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
return process.stdout.pipe(
|
|
219
|
+
Stream.decodeText({ encoding: 'utf8' }),
|
|
220
|
+
Stream.splitLines,
|
|
221
|
+
Stream.mapError(
|
|
222
|
+
(cause) =>
|
|
223
|
+
new DockerComposeError({
|
|
224
|
+
cause,
|
|
225
|
+
note: `Error reading Docker Compose logs in ${cwd}`,
|
|
226
|
+
}),
|
|
227
|
+
),
|
|
228
|
+
)
|
|
229
|
+
}).pipe(Stream.unwrap)
|
|
230
|
+
|
|
231
|
+
const down = Effect.fn('downDockerCompose')(function* (options?: {
|
|
232
|
+
readonly env?: Record<string, string>
|
|
233
|
+
readonly volumes?: boolean
|
|
234
|
+
readonly removeOrphans?: boolean
|
|
235
|
+
}) {
|
|
236
|
+
yield* Effect.log(`Tearing down Docker Compose services in ${cwd}`)
|
|
237
|
+
|
|
238
|
+
const downArgs = ['down']
|
|
239
|
+
if (options?.volumes === true) downArgs.push('-v')
|
|
240
|
+
if (options?.removeOrphans === true) downArgs.push('--remove-orphans')
|
|
241
|
+
if (serviceName !== undefined) downArgs.push(serviceName)
|
|
242
|
+
|
|
243
|
+
const exitCode = yield* spawner.exitCode(composeCommand(downArgs, envOptions(options?.env)))
|
|
244
|
+
|
|
245
|
+
if (Number(exitCode) !== 0) {
|
|
246
|
+
return yield* new DockerComposeError({
|
|
247
|
+
cause: new Error(`Docker compose down exited with code ${exitCode}`),
|
|
248
|
+
note: `Failed to tear down Docker Compose services`,
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
yield* Effect.log(`Docker Compose services torn down successfully`)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
// Register cleanup finalizer to ensure containers are removed when scope closes
|
|
256
|
+
yield* Effect.addFinalizer(() =>
|
|
257
|
+
down({ volumes: true, removeOrphans: true }).pipe(
|
|
258
|
+
Effect.tap(() => Effect.log(`Docker Compose cleanup completed for project ${projectName}`)),
|
|
259
|
+
Effect.catchCause((cause) =>
|
|
260
|
+
Effect.log('Docker Compose cleanup failed for project', projectName, objectToString(cause)),
|
|
261
|
+
),
|
|
262
|
+
),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
return DockerCompose.of({ pull, start, stop, down, logs, projectName })
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
export const layer = (options: Options) => Layer.effect(DockerCompose, make(options))
|
|
269
|
+
|
|
270
|
+
const performHealthCheck = ({
|
|
271
|
+
url,
|
|
272
|
+
timeout = Duration.minutes(2),
|
|
273
|
+
interval = Duration.seconds(2),
|
|
274
|
+
}: {
|
|
275
|
+
url: string
|
|
276
|
+
timeout?: Duration.Duration
|
|
277
|
+
interval?: Duration.Duration
|
|
278
|
+
}): Effect.Effect<void, DockerComposeError, ChildProcessSpawner.ChildProcessSpawner | Scope.Scope> =>
|
|
279
|
+
Effect.gen(function* () {
|
|
280
|
+
yield* Effect.log(`Performing health check on ${url}`)
|
|
281
|
+
|
|
282
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
|
283
|
+
|
|
284
|
+
const checkHealth = spawner.exitCode(ChildProcess.make('curl', ['-f', '-s', url])).pipe(
|
|
285
|
+
Effect.map((code: number) => code === 0),
|
|
286
|
+
Effect.orElseSucceed(() => false),
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
const healthCheck = checkHealth.pipe(
|
|
290
|
+
Effect.repeat({
|
|
291
|
+
while: (healthy: boolean) => healthy === false,
|
|
292
|
+
schedule: Schedule.fixed(interval),
|
|
293
|
+
}),
|
|
294
|
+
Effect.timeout(timeout),
|
|
295
|
+
Effect.mapError(
|
|
296
|
+
() =>
|
|
297
|
+
new DockerComposeError({
|
|
298
|
+
cause: new Error('Health check timeout'),
|
|
299
|
+
note: `Health check failed for ${url} after ${Duration.toMillis(timeout)}ms`,
|
|
300
|
+
}),
|
|
301
|
+
),
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
yield* healthCheck
|
|
305
|
+
yield* Effect.log(`Health check passed for ${url}`)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
// Convenience function for scoped Docker Compose operations with automatic cleanup
|
|
309
|
+
export const startDockerComposeServicesScoped = (
|
|
310
|
+
args: Options & {
|
|
311
|
+
healthCheck?: StartOptions['healthCheck']
|
|
312
|
+
},
|
|
313
|
+
): Effect.Effect<
|
|
314
|
+
void,
|
|
315
|
+
DockerComposeError | PlatformError.PlatformError,
|
|
316
|
+
DockerCompose | ChildProcessSpawner.ChildProcessSpawner | Scope.Scope
|
|
317
|
+
> =>
|
|
318
|
+
Effect.gen(function* () {
|
|
319
|
+
const dockerCompose = yield* DockerCompose
|
|
320
|
+
|
|
321
|
+
// Start the services
|
|
322
|
+
yield* dockerCompose.start({
|
|
323
|
+
...omitUndefineds({ healthCheck: args.healthCheck !== undefined ? args.healthCheck : undefined }),
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
// Add cleanup finalizer to the current scope
|
|
327
|
+
yield* Effect.addFinalizer(() => dockerCompose.stop.pipe(Effect.orDie))
|
|
328
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * as DockerCompose from './DockerCompose.ts'
|
package/src/node/FileLogger.ts
CHANGED
|
@@ -1,48 +1,8 @@
|
|
|
1
|
-
import * as fs from 'node:fs'
|
|
2
|
-
import path from 'node:path'
|
|
3
1
|
import util from 'node:util'
|
|
4
2
|
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
FiberId,
|
|
9
|
-
HashMap,
|
|
10
|
-
Inspectable,
|
|
11
|
-
Layer,
|
|
12
|
-
List,
|
|
13
|
-
Logger,
|
|
14
|
-
type LogLevel,
|
|
15
|
-
LogSpan,
|
|
16
|
-
ReadonlyArray,
|
|
17
|
-
} from '@livestore/utils/effect'
|
|
18
|
-
|
|
19
|
-
export const makeFileLogger = (
|
|
20
|
-
logFilePath: string,
|
|
21
|
-
options?: {
|
|
22
|
-
readonly threadName: string
|
|
23
|
-
readonly colors?: boolean
|
|
24
|
-
},
|
|
25
|
-
) =>
|
|
26
|
-
Layer.unwrapScoped(
|
|
27
|
-
Effect.gen(function* () {
|
|
28
|
-
yield* Effect.sync(() => fs.mkdirSync(path.dirname(logFilePath), { recursive: true }))
|
|
29
|
-
|
|
30
|
-
const logFile = yield* Effect.acquireRelease(
|
|
31
|
-
Effect.sync(() => fs.openSync(logFilePath, 'a', 0o666)),
|
|
32
|
-
(fd) => Effect.sync(() => fs.closeSync(fd)),
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
return Logger.replace(
|
|
36
|
-
Logger.defaultLogger,
|
|
37
|
-
prettyLoggerTty({
|
|
38
|
-
colors: options?.colors ?? false,
|
|
39
|
-
stderr: false,
|
|
40
|
-
formatDate: (date) => `${defaultDateFormat(date)} ${options?.threadName ?? ''}`,
|
|
41
|
-
onLog: (str) => fs.writeSync(logFile, str),
|
|
42
|
-
}),
|
|
43
|
-
)
|
|
44
|
-
}),
|
|
45
|
-
)
|
|
3
|
+
import { Redactable } from 'effect'
|
|
4
|
+
|
|
5
|
+
import { Cause, Inspectable, Logger, type LogLevel, References, ReadonlyArray } from '@livestore/utils/effect'
|
|
46
6
|
|
|
47
7
|
const withColor = (text: string, ...colors: ReadonlyArray<string>) => {
|
|
48
8
|
let out = ''
|
|
@@ -66,18 +26,18 @@ const colors = {
|
|
|
66
26
|
bgBrightRed: '101',
|
|
67
27
|
} as const
|
|
68
28
|
|
|
69
|
-
const logLevelColors: Record<LogLevel.LogLevel
|
|
29
|
+
const logLevelColors: Record<LogLevel.LogLevel, ReadonlyArray<string>> = {
|
|
70
30
|
None: [],
|
|
71
31
|
All: [],
|
|
72
32
|
Trace: [colors.gray],
|
|
73
33
|
Debug: [colors.blue],
|
|
74
34
|
Info: [colors.green],
|
|
75
|
-
|
|
35
|
+
Warn: [colors.yellow],
|
|
76
36
|
Error: [colors.red],
|
|
77
37
|
Fatal: [colors.bgBrightRed, colors.black],
|
|
78
38
|
}
|
|
79
39
|
|
|
80
|
-
export const
|
|
40
|
+
export const formatLogTime = (date: Date): string =>
|
|
81
41
|
`${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date
|
|
82
42
|
.getSeconds()
|
|
83
43
|
.toString()
|
|
@@ -91,7 +51,7 @@ export const structuredMessage = (u: unknown): unknown => {
|
|
|
91
51
|
return String(u)
|
|
92
52
|
}
|
|
93
53
|
default: {
|
|
94
|
-
return Inspectable.
|
|
54
|
+
return Inspectable.toJson(u)
|
|
95
55
|
}
|
|
96
56
|
}
|
|
97
57
|
}
|
|
@@ -115,93 +75,108 @@ const consoleLogToString = (...s: any[]) => {
|
|
|
115
75
|
.join(' ')
|
|
116
76
|
}
|
|
117
77
|
|
|
118
|
-
|
|
78
|
+
type PrettyLoggerOptions = {
|
|
119
79
|
readonly colors: boolean
|
|
120
80
|
readonly stderr: boolean
|
|
121
81
|
readonly formatDate: (date: Date) => string
|
|
122
82
|
readonly onLog?: (str: string) => void
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const formatPrettyLog = ({
|
|
86
|
+
annotations,
|
|
87
|
+
cause,
|
|
88
|
+
date,
|
|
89
|
+
fiberId,
|
|
90
|
+
logLevel,
|
|
91
|
+
message: message_,
|
|
92
|
+
options,
|
|
93
|
+
spans,
|
|
94
|
+
}: {
|
|
95
|
+
readonly annotations: Readonly<Record<string, unknown>>
|
|
96
|
+
readonly cause: Cause.Cause<unknown>
|
|
97
|
+
readonly date: Date
|
|
98
|
+
readonly fiberId: number
|
|
99
|
+
readonly logLevel: LogLevel.LogLevel
|
|
100
|
+
readonly message: unknown
|
|
101
|
+
readonly options: PrettyLoggerOptions
|
|
102
|
+
readonly spans: ReadonlyArray<readonly [label: string, timestamp: number]>
|
|
123
103
|
}) => {
|
|
124
104
|
const color = options.colors === true ? withColor : withColorNoop
|
|
125
|
-
|
|
126
|
-
let str = ''
|
|
105
|
+
let str = ''
|
|
127
106
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
107
|
+
const log = (...s: any[]) => {
|
|
108
|
+
str += `${consoleLogToString(...s)}\n`
|
|
109
|
+
options.onLog?.(str)
|
|
110
|
+
}
|
|
132
111
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
112
|
+
const logIndented = (...s: any[]) => {
|
|
113
|
+
str += `${consoleLogToString(...s).replace(/^/gm, ' ')}\n`
|
|
114
|
+
options.onLog?.(str)
|
|
115
|
+
}
|
|
137
116
|
|
|
138
|
-
|
|
117
|
+
const message = ReadonlyArray.ensure(message_)
|
|
139
118
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
119
|
+
let firstLine =
|
|
120
|
+
color(`[${options.formatDate(date)}]`, colors.white) +
|
|
121
|
+
` ${color(logLevel.toUpperCase(), ...logLevelColors[logLevel])}` +
|
|
122
|
+
` (#${fiberId})`
|
|
144
123
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
firstLine += ` ${render(span)}`
|
|
150
|
-
}
|
|
124
|
+
if (spans.length > 0) {
|
|
125
|
+
const now = date.getTime()
|
|
126
|
+
for (const [label, timestamp] of spans) {
|
|
127
|
+
firstLine += ` ${label}=${now - timestamp}ms`
|
|
151
128
|
}
|
|
129
|
+
}
|
|
152
130
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
131
|
+
firstLine += ':'
|
|
132
|
+
let messageIndex = 0
|
|
133
|
+
if (message.length > 0) {
|
|
134
|
+
const firstMaybeString = structuredMessage(message[0])
|
|
135
|
+
if (typeof firstMaybeString === 'string') {
|
|
136
|
+
firstLine += ` ${color(firstMaybeString, colors.bold, colors.cyan)}`
|
|
137
|
+
messageIndex++
|
|
161
138
|
}
|
|
139
|
+
}
|
|
162
140
|
|
|
163
|
-
|
|
164
|
-
|
|
141
|
+
log(firstLine)
|
|
142
|
+
// if (!processIsBun) console.group()
|
|
165
143
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
144
|
+
if (cause.reasons.length > 0) {
|
|
145
|
+
logIndented(Cause.pretty(cause))
|
|
146
|
+
}
|
|
169
147
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
148
|
+
if (messageIndex < message.length) {
|
|
149
|
+
for (; messageIndex < message.length; messageIndex++) {
|
|
150
|
+
const msg = message[messageIndex]
|
|
151
|
+
if (typeof msg === 'object' && msg !== null) {
|
|
152
|
+
logIndented(
|
|
153
|
+
util.inspect(structuredMessage(msg), {
|
|
154
|
+
depth: 3,
|
|
155
|
+
colors: false,
|
|
156
|
+
compact: false,
|
|
157
|
+
breakLength: 120,
|
|
158
|
+
}),
|
|
159
|
+
)
|
|
160
|
+
} else {
|
|
161
|
+
logIndented(Redactable.redact(msg))
|
|
185
162
|
}
|
|
186
163
|
}
|
|
164
|
+
}
|
|
187
165
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
}
|
|
166
|
+
for (const [key, value] of Object.entries(annotations)) {
|
|
167
|
+
const formattedValue =
|
|
168
|
+
typeof value === 'object' && value !== null
|
|
169
|
+
? util.inspect(structuredMessage(value), {
|
|
170
|
+
depth: 3,
|
|
171
|
+
colors: false,
|
|
172
|
+
compact: false,
|
|
173
|
+
breakLength: 120,
|
|
174
|
+
})
|
|
175
|
+
: Redactable.redact(value)
|
|
176
|
+
logIndented(color(`${key}:`, colors.bold, colors.white), formattedValue)
|
|
177
|
+
}
|
|
202
178
|
|
|
203
|
-
|
|
179
|
+
// if (!processIsBun) console.groupEnd()
|
|
204
180
|
|
|
205
|
-
|
|
206
|
-
})
|
|
181
|
+
return str
|
|
207
182
|
}
|
package/src/node/cmd-log.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import { Effect } from '@livestore/utils/effect'
|
|
4
|
+
import { Cause, Effect, Predicate } from '@livestore/utils/effect'
|
|
6
5
|
|
|
7
6
|
export type TCmdLoggingOptions = {
|
|
8
7
|
readonly logDir?: string
|
|
@@ -31,18 +30,27 @@ export const prepareCmdLogging: (options: TCmdLoggingOptions) => Effect.Effect<s
|
|
|
31
30
|
const safeIso = new Date().toISOString().replaceAll(':', '-')
|
|
32
31
|
const archivedBase = `${path.parse(logFileName).name}-${safeIso}.log`
|
|
33
32
|
const archivedLog = path.join(archiveDir, archivedBase)
|
|
34
|
-
yield* Effect.try(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
yield* Effect.try({
|
|
34
|
+
try: () => fs.renameSync(currentLogPath, archivedLog),
|
|
35
|
+
catch: (cause) => new Cause.UnknownError(cause),
|
|
36
|
+
}).pipe(
|
|
37
|
+
Effect.catch(() =>
|
|
38
|
+
Effect.try({
|
|
39
|
+
try: () => {
|
|
40
|
+
fs.copyFileSync(currentLogPath, archivedLog)
|
|
41
|
+
fs.truncateSync(currentLogPath, 0)
|
|
42
|
+
},
|
|
43
|
+
catch: (cause) => new Cause.UnknownError(cause),
|
|
39
44
|
}),
|
|
40
45
|
),
|
|
41
46
|
Effect.ignore,
|
|
42
47
|
)
|
|
43
48
|
|
|
44
49
|
// Prune archives to retain only the newest N
|
|
45
|
-
yield* Effect.try(
|
|
50
|
+
yield* Effect.try({
|
|
51
|
+
try: () => fs.readdirSync(archiveDir),
|
|
52
|
+
catch: (cause) => new Cause.UnknownError(cause),
|
|
53
|
+
}).pipe(
|
|
46
54
|
Effect.map((names) => names.filter((n) => n.endsWith('.log'))),
|
|
47
55
|
Effect.map((names) =>
|
|
48
56
|
names
|
|
@@ -51,7 +59,10 @@ export const prepareCmdLogging: (options: TCmdLoggingOptions) => Effect.Effect<s
|
|
|
51
59
|
),
|
|
52
60
|
Effect.flatMap((entries) =>
|
|
53
61
|
Effect.forEach(entries.slice(logRetention), (e) =>
|
|
54
|
-
Effect.try(
|
|
62
|
+
Effect.try({
|
|
63
|
+
try: () => fs.unlinkSync(path.join(archiveDir, e.name)),
|
|
64
|
+
catch: (cause) => new Cause.UnknownError(cause),
|
|
65
|
+
}).pipe(Effect.ignore),
|
|
55
66
|
),
|
|
56
67
|
),
|
|
57
68
|
Effect.ignore,
|
|
@@ -71,7 +82,7 @@ export const applyLoggingToCommand: (
|
|
|
71
82
|
) => Effect.Effect<{ input: string | string[]; subshell: boolean; logPath?: string }> = Effect.fn('cmd.logging.apply')(
|
|
72
83
|
function* (commandInput, options) {
|
|
73
84
|
const asArray = Array.isArray(commandInput)
|
|
74
|
-
const parts = asArray === true ? commandInput.filter(isNotUndefined) : undefined
|
|
85
|
+
const parts = asArray === true ? commandInput.filter(Predicate.isNotUndefined) : undefined
|
|
75
86
|
|
|
76
87
|
const logPath = yield* prepareCmdLogging(options)
|
|
77
88
|
|