@livestore/utils-dev 0.4.0-dev.9 → 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 -0
- 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} +18 -18
- 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 +64 -80
- package/dist/node/FileLogger.js.map +1 -1
- package/dist/node/cmd-log.d.ts +21 -0
- package/dist/node/cmd-log.d.ts.map +1 -0
- package/dist/node/cmd-log.js +58 -0
- package/dist/node/cmd-log.js.map +1 -0
- package/dist/node/cmd.d.ts +20 -14
- package/dist/node/cmd.d.ts.map +1 -1
- package/dist/node/cmd.js +248 -33
- package/dist/node/cmd.js.map +1 -1
- package/dist/node/cmd.test.d.ts +2 -0
- package/dist/node/cmd.test.d.ts.map +1 -0
- package/dist/node/cmd.test.js +103 -0
- package/dist/node/cmd.test.js.map +1 -0
- package/dist/node/mod.d.ts +5 -5
- package/dist/node/mod.d.ts.map +1 -1
- package/dist/node/mod.js +114 -66
- package/dist/node/mod.js.map +1 -1
- package/dist/node/workspace.d.ts +22 -0
- package/dist/node/workspace.d.ts.map +1 -0
- package/dist/node/workspace.js +26 -0
- package/dist/node/workspace.js.map +1 -0
- 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 +16 -16
- package/dist/node-vitest/Vitest.js.map +1 -1
- package/dist/node-vitest/Vitest.test.d.ts +6 -0
- package/dist/node-vitest/Vitest.test.d.ts.map +1 -1
- package/dist/node-vitest/Vitest.test.js +25 -10
- package/dist/node-vitest/Vitest.test.js.map +1 -1
- package/dist/wrangler/WranglerDevServer.d.ts +32 -20
- package/dist/wrangler/WranglerDevServer.d.ts.map +1 -1
- package/dist/wrangler/WranglerDevServer.js +79 -60
- package/dist/wrangler/WranglerDevServer.js.map +1 -1
- package/dist/wrangler/WranglerDevServer.test.js +31 -31
- 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 +56 -21
- package/src/node/{DockerComposeService/DockerComposeService.test.ts → DockerCompose/DockerCompose.test.ts} +23 -20
- package/src/node/DockerCompose/DockerCompose.ts +328 -0
- package/src/node/DockerCompose/mod.ts +1 -0
- package/src/node/DockerCompose/test-fixtures/docker-compose.yml +4 -0
- package/src/node/FileLogger.ts +91 -115
- package/src/node/cmd-log.ts +95 -0
- package/src/node/cmd.test.ts +136 -0
- package/src/node/cmd.ts +431 -82
- package/src/node/mod.ts +152 -96
- package/src/node/workspace.ts +45 -0
- package/src/node-vitest/Vitest.test.ts +34 -10
- package/src/node-vitest/Vitest.ts +60 -66
- package/src/wrangler/WranglerDevServer.test.ts +38 -40
- package/src/wrangler/WranglerDevServer.ts +165 -108
- package/src/wrangler/fixtures/wrangler.toml +1 -1
- package/src/wrangler/mod.ts +1 -6
- package/dist/.tsbuildinfo.json +0 -1
- package/dist/node/DockerComposeService/DockerComposeService.d.ts +0 -58
- package/dist/node/DockerComposeService/DockerComposeService.d.ts.map +0 -1
- package/dist/node/DockerComposeService/DockerComposeService.js +0 -144
- 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 -328
- package/src/node/DockerComposeService/test-fixtures/docker-compose.yml +0 -4
package/src/node/FileLogger.ts
CHANGED
|
@@ -1,47 +1,8 @@
|
|
|
1
|
-
import * as fs from 'node:fs'
|
|
2
|
-
import path from 'node:path'
|
|
3
1
|
import util from 'node:util'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
HashMap,
|
|
9
|
-
Inspectable,
|
|
10
|
-
Layer,
|
|
11
|
-
List,
|
|
12
|
-
Logger,
|
|
13
|
-
type LogLevel,
|
|
14
|
-
LogSpan,
|
|
15
|
-
ReadonlyArray,
|
|
16
|
-
} from '@livestore/utils/effect'
|
|
17
|
-
|
|
18
|
-
export const makeFileLogger = (
|
|
19
|
-
logFilePath: string,
|
|
20
|
-
options?: {
|
|
21
|
-
readonly threadName: string
|
|
22
|
-
readonly colors?: boolean
|
|
23
|
-
},
|
|
24
|
-
) =>
|
|
25
|
-
Layer.unwrapScoped(
|
|
26
|
-
Effect.gen(function* () {
|
|
27
|
-
yield* Effect.sync(() => fs.mkdirSync(path.dirname(logFilePath), { recursive: true }))
|
|
28
|
-
|
|
29
|
-
const logFile = yield* Effect.acquireRelease(
|
|
30
|
-
Effect.sync(() => fs.openSync(logFilePath, 'a', 0o666)),
|
|
31
|
-
(fd) => Effect.sync(() => fs.closeSync(fd)),
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
return Logger.replace(
|
|
35
|
-
Logger.defaultLogger,
|
|
36
|
-
prettyLoggerTty({
|
|
37
|
-
colors: options?.colors ?? false,
|
|
38
|
-
stderr: false,
|
|
39
|
-
formatDate: (date) => `${defaultDateFormat(date)} ${options?.threadName ?? ''}`,
|
|
40
|
-
onLog: (str) => fs.writeSync(logFile, str),
|
|
41
|
-
}),
|
|
42
|
-
)
|
|
43
|
-
}),
|
|
44
|
-
)
|
|
2
|
+
|
|
3
|
+
import { Redactable } from 'effect'
|
|
4
|
+
|
|
5
|
+
import { Cause, Inspectable, Logger, type LogLevel, References, ReadonlyArray } from '@livestore/utils/effect'
|
|
45
6
|
|
|
46
7
|
const withColor = (text: string, ...colors: ReadonlyArray<string>) => {
|
|
47
8
|
let out = ''
|
|
@@ -65,18 +26,18 @@ const colors = {
|
|
|
65
26
|
bgBrightRed: '101',
|
|
66
27
|
} as const
|
|
67
28
|
|
|
68
|
-
const logLevelColors: Record<LogLevel.LogLevel
|
|
29
|
+
const logLevelColors: Record<LogLevel.LogLevel, ReadonlyArray<string>> = {
|
|
69
30
|
None: [],
|
|
70
31
|
All: [],
|
|
71
32
|
Trace: [colors.gray],
|
|
72
33
|
Debug: [colors.blue],
|
|
73
34
|
Info: [colors.green],
|
|
74
|
-
|
|
35
|
+
Warn: [colors.yellow],
|
|
75
36
|
Error: [colors.red],
|
|
76
37
|
Fatal: [colors.bgBrightRed, colors.black],
|
|
77
38
|
}
|
|
78
39
|
|
|
79
|
-
export const
|
|
40
|
+
export const formatLogTime = (date: Date): string =>
|
|
80
41
|
`${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date
|
|
81
42
|
.getSeconds()
|
|
82
43
|
.toString()
|
|
@@ -90,7 +51,7 @@ export const structuredMessage = (u: unknown): unknown => {
|
|
|
90
51
|
return String(u)
|
|
91
52
|
}
|
|
92
53
|
default: {
|
|
93
|
-
return Inspectable.
|
|
54
|
+
return Inspectable.toJson(u)
|
|
94
55
|
}
|
|
95
56
|
}
|
|
96
57
|
}
|
|
@@ -114,93 +75,108 @@ const consoleLogToString = (...s: any[]) => {
|
|
|
114
75
|
.join(' ')
|
|
115
76
|
}
|
|
116
77
|
|
|
117
|
-
|
|
78
|
+
type PrettyLoggerOptions = {
|
|
118
79
|
readonly colors: boolean
|
|
119
80
|
readonly stderr: boolean
|
|
120
81
|
readonly formatDate: (date: Date) => string
|
|
121
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]>
|
|
122
103
|
}) => {
|
|
123
|
-
const color = options.colors ? withColor : withColorNoop
|
|
124
|
-
|
|
125
|
-
let str = ''
|
|
104
|
+
const color = options.colors === true ? withColor : withColorNoop
|
|
105
|
+
let str = ''
|
|
126
106
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
107
|
+
const log = (...s: any[]) => {
|
|
108
|
+
str += `${consoleLogToString(...s)}\n`
|
|
109
|
+
options.onLog?.(str)
|
|
110
|
+
}
|
|
131
111
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
112
|
+
const logIndented = (...s: any[]) => {
|
|
113
|
+
str += `${consoleLogToString(...s).replace(/^/gm, ' ')}\n`
|
|
114
|
+
options.onLog?.(str)
|
|
115
|
+
}
|
|
136
116
|
|
|
137
|
-
|
|
117
|
+
const message = ReadonlyArray.ensure(message_)
|
|
138
118
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
119
|
+
let firstLine =
|
|
120
|
+
color(`[${options.formatDate(date)}]`, colors.white) +
|
|
121
|
+
` ${color(logLevel.toUpperCase(), ...logLevelColors[logLevel])}` +
|
|
122
|
+
` (#${fiberId})`
|
|
143
123
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
firstLine += ` ${render(span)}`
|
|
149
|
-
}
|
|
124
|
+
if (spans.length > 0) {
|
|
125
|
+
const now = date.getTime()
|
|
126
|
+
for (const [label, timestamp] of spans) {
|
|
127
|
+
firstLine += ` ${label}=${now - timestamp}ms`
|
|
150
128
|
}
|
|
129
|
+
}
|
|
151
130
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
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++
|
|
160
138
|
}
|
|
139
|
+
}
|
|
161
140
|
|
|
162
|
-
|
|
163
|
-
|
|
141
|
+
log(firstLine)
|
|
142
|
+
// if (!processIsBun) console.group()
|
|
164
143
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
144
|
+
if (cause.reasons.length > 0) {
|
|
145
|
+
logIndented(Cause.pretty(cause))
|
|
146
|
+
}
|
|
168
147
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
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))
|
|
184
162
|
}
|
|
185
163
|
}
|
|
164
|
+
}
|
|
186
165
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
}
|
|
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
|
+
}
|
|
201
178
|
|
|
202
|
-
|
|
179
|
+
// if (!processIsBun) console.groupEnd()
|
|
203
180
|
|
|
204
|
-
|
|
205
|
-
})
|
|
181
|
+
return str
|
|
206
182
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { Cause, Effect, Predicate } from '@livestore/utils/effect'
|
|
5
|
+
|
|
6
|
+
export type TCmdLoggingOptions = {
|
|
7
|
+
readonly logDir?: string
|
|
8
|
+
readonly logFileName?: string
|
|
9
|
+
readonly logRetention?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Prepares logging directories, archives previous canonical log and prunes archives.
|
|
14
|
+
* Returns the canonical current log path if logging is enabled, otherwise undefined.
|
|
15
|
+
*/
|
|
16
|
+
export const prepareCmdLogging: (options: TCmdLoggingOptions) => Effect.Effect<string | undefined> = Effect.fn(
|
|
17
|
+
'cmd.logging.prepare',
|
|
18
|
+
)(function* ({ logDir, logFileName = 'dev.log', logRetention = 50 }: TCmdLoggingOptions) {
|
|
19
|
+
if (logDir == null || logDir === '') return undefined as string | undefined
|
|
20
|
+
|
|
21
|
+
const logsDir = logDir
|
|
22
|
+
const archiveDir = path.join(logsDir, 'archive')
|
|
23
|
+
const currentLogPath = path.join(logsDir, logFileName)
|
|
24
|
+
|
|
25
|
+
// Ensure directories exist
|
|
26
|
+
yield* Effect.sync(() => fs.mkdirSync(archiveDir, { recursive: true }))
|
|
27
|
+
|
|
28
|
+
// Archive previous log if present
|
|
29
|
+
if (fs.existsSync(currentLogPath) === true) {
|
|
30
|
+
const safeIso = new Date().toISOString().replaceAll(':', '-')
|
|
31
|
+
const archivedBase = `${path.parse(logFileName).name}-${safeIso}.log`
|
|
32
|
+
const archivedLog = path.join(archiveDir, archivedBase)
|
|
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),
|
|
44
|
+
}),
|
|
45
|
+
),
|
|
46
|
+
Effect.ignore,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
// Prune archives to retain only the newest N
|
|
50
|
+
yield* Effect.try({
|
|
51
|
+
try: () => fs.readdirSync(archiveDir),
|
|
52
|
+
catch: (cause) => new Cause.UnknownError(cause),
|
|
53
|
+
}).pipe(
|
|
54
|
+
Effect.map((names) => names.filter((n) => n.endsWith('.log'))),
|
|
55
|
+
Effect.map((names) =>
|
|
56
|
+
names
|
|
57
|
+
.map((name) => ({ name, mtimeMs: fs.statSync(path.join(archiveDir, name)).mtimeMs }))
|
|
58
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs),
|
|
59
|
+
),
|
|
60
|
+
Effect.flatMap((entries) =>
|
|
61
|
+
Effect.forEach(entries.slice(logRetention), (e) =>
|
|
62
|
+
Effect.try({
|
|
63
|
+
try: () => fs.unlinkSync(path.join(archiveDir, e.name)),
|
|
64
|
+
catch: (cause) => new Cause.UnknownError(cause),
|
|
65
|
+
}).pipe(Effect.ignore),
|
|
66
|
+
),
|
|
67
|
+
),
|
|
68
|
+
Effect.ignore,
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return currentLogPath
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Given a command input, applies logging by piping output through `tee` to the
|
|
77
|
+
* canonical log file. Returns the transformed input and whether a shell is required.
|
|
78
|
+
*/
|
|
79
|
+
export const applyLoggingToCommand: (
|
|
80
|
+
commandInput: string | (string | undefined)[],
|
|
81
|
+
options: TCmdLoggingOptions,
|
|
82
|
+
) => Effect.Effect<{ input: string | string[]; subshell: boolean; logPath?: string }> = Effect.fn('cmd.logging.apply')(
|
|
83
|
+
function* (commandInput, options) {
|
|
84
|
+
const asArray = Array.isArray(commandInput)
|
|
85
|
+
const parts = asArray === true ? commandInput.filter(Predicate.isNotUndefined) : undefined
|
|
86
|
+
|
|
87
|
+
const logPath = yield* prepareCmdLogging(options)
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
input: asArray === true ? ((parts as string[]) ?? []) : commandInput,
|
|
91
|
+
subshell: false,
|
|
92
|
+
...(logPath !== undefined ? { logPath } : {}),
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { expect } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import { Vitest } from '@livestore/utils-dev/node-vitest'
|
|
7
|
+
import { ChildProcessSpawner, Duration, Effect, Layer } from '@livestore/utils/effect'
|
|
8
|
+
import { PlatformNode } from '@livestore/utils/node'
|
|
9
|
+
|
|
10
|
+
import { cmd } from './cmd.ts'
|
|
11
|
+
import { CurrentWorkingDirectory } from './workspace.ts'
|
|
12
|
+
|
|
13
|
+
const withNode = Vitest.makeWithTestCtx({
|
|
14
|
+
makeLayer: () => Layer.mergeAll(PlatformNode.NodeServices.layer, CurrentWorkingDirectory.live),
|
|
15
|
+
timeout: 20_000,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
Vitest.describe('cmd helper', () => {
|
|
19
|
+
const ansiRegex = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g')
|
|
20
|
+
|
|
21
|
+
Vitest.live('runs tokenized string without shell', (test) =>
|
|
22
|
+
Effect.gen(function* () {
|
|
23
|
+
const exit = yield* cmd('printf ok')
|
|
24
|
+
expect(exit).toBe(ChildProcessSpawner.ExitCode(0))
|
|
25
|
+
}).pipe(withNode(test)),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
Vitest.live('runs array input', (test) =>
|
|
29
|
+
Effect.gen(function* () {
|
|
30
|
+
const exit = yield* cmd(['printf', 'ok'])
|
|
31
|
+
expect(exit).toBe(ChildProcessSpawner.ExitCode(0))
|
|
32
|
+
}).pipe(withNode(test)),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
/** TODO(#1020): Investigate CI timeout flakiness for cmd tests that spawn many child processes */
|
|
36
|
+
Vitest.live(
|
|
37
|
+
'supports logging with archive + retention',
|
|
38
|
+
(test) =>
|
|
39
|
+
Effect.gen(function* () {
|
|
40
|
+
const workspace = process.env.WORKSPACE_ROOT!
|
|
41
|
+
const logsDir = path.join(workspace, 'tmp', 'cmd-tests', String(Date.now()))
|
|
42
|
+
|
|
43
|
+
// first run
|
|
44
|
+
const exit1 = yield* cmd('printf first', { logDir: logsDir })
|
|
45
|
+
expect(exit1).toBe(ChildProcessSpawner.ExitCode(0))
|
|
46
|
+
const current = path.join(logsDir, 'dev.log')
|
|
47
|
+
expect(fs.existsSync(current)).toBe(true)
|
|
48
|
+
const firstLog = fs.readFileSync(current, 'utf8')
|
|
49
|
+
const firstStdoutLines = firstLog.split('\n').filter((line) => line.includes('[stdout]'))
|
|
50
|
+
expect(firstStdoutLines.length).toBeGreaterThan(0)
|
|
51
|
+
for (const line of firstStdoutLines) {
|
|
52
|
+
expect(line).toContain('[stdout] first')
|
|
53
|
+
expect(line).toMatch(/Info/i)
|
|
54
|
+
expect(line).toContain('printf first')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// second run — archives previous
|
|
58
|
+
const exit2 = yield* cmd('printf second', { logDir: logsDir })
|
|
59
|
+
expect(exit2).toBe(ChildProcessSpawner.ExitCode(0))
|
|
60
|
+
const archiveDir = path.join(logsDir, 'archive')
|
|
61
|
+
const archives = fs.readdirSync(archiveDir).filter((f) => f.endsWith('.log'))
|
|
62
|
+
expect(archives.length).toBe(1)
|
|
63
|
+
const archivedPath = path.join(archiveDir, archives[0]!)
|
|
64
|
+
const archivedLog = fs.readFileSync(archivedPath, 'utf8')
|
|
65
|
+
const archivedStdoutLines = archivedLog.split('\n').filter((line) => line.includes('[stdout]'))
|
|
66
|
+
expect(archivedStdoutLines.length).toBeGreaterThan(0)
|
|
67
|
+
for (const line of archivedStdoutLines) {
|
|
68
|
+
expect(line).toContain('[stdout] first')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const secondLog = fs.readFileSync(current, 'utf8')
|
|
72
|
+
const secondStdoutLines = secondLog.split('\n').filter((line) => line.includes('[stdout]'))
|
|
73
|
+
expect(secondStdoutLines.length).toBeGreaterThan(0)
|
|
74
|
+
for (const line of secondStdoutLines) {
|
|
75
|
+
expect(line).toContain('[stdout] second')
|
|
76
|
+
expect(line).toMatch(/Info/i)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// generate many archives to exercise retention (keep 50)
|
|
80
|
+
for (let i = 0; i < 60; i++) {
|
|
81
|
+
// Use small unique payloads
|
|
82
|
+
yield* cmd(['printf', String(i)], { logDir: logsDir })
|
|
83
|
+
}
|
|
84
|
+
const archivesAfter = fs.readdirSync(archiveDir).filter((f) => f.endsWith('.log'))
|
|
85
|
+
expect(archivesAfter.length).toBeLessThanOrEqual(50)
|
|
86
|
+
}).pipe(withNode(test)),
|
|
87
|
+
{ timeout: 30_000, retry: 3 },
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
Vitest.live('streams stdout and stderr with logger formatting', (test) =>
|
|
91
|
+
Effect.gen(function* () {
|
|
92
|
+
const workspace = process.env.WORKSPACE_ROOT!
|
|
93
|
+
const logsDir = path.join(workspace, 'tmp', 'cmd-tests', `format-${Date.now()}`)
|
|
94
|
+
|
|
95
|
+
const exit = yield* cmd(['node', '-e', "console.log('out'); console.error('err')"], {
|
|
96
|
+
logDir: logsDir,
|
|
97
|
+
})
|
|
98
|
+
expect(exit).toBe(ChildProcessSpawner.ExitCode(0))
|
|
99
|
+
|
|
100
|
+
const current = path.join(logsDir, 'dev.log')
|
|
101
|
+
const logContent = fs.readFileSync(current, 'utf8')
|
|
102
|
+
expect(logContent).toMatch(/\[stdout] out/)
|
|
103
|
+
expect(logContent).toMatch(/\[stderr] err/)
|
|
104
|
+
|
|
105
|
+
const relevantLines = logContent
|
|
106
|
+
.split('\n')
|
|
107
|
+
.map((line) => line.trim())
|
|
108
|
+
.filter((line) => line.includes('[stdout]') || line.includes('[stderr]'))
|
|
109
|
+
|
|
110
|
+
expect(relevantLines.length).toBeGreaterThanOrEqual(2)
|
|
111
|
+
|
|
112
|
+
for (const line of relevantLines) {
|
|
113
|
+
const stripped = line.replace(ansiRegex, '')
|
|
114
|
+
expect(stripped.startsWith('[')).toBe(true)
|
|
115
|
+
expect(stripped).toMatch(/(Info|Warn)/i)
|
|
116
|
+
expect(stripped).toMatch(/\[(stdout|stderr)]/)
|
|
117
|
+
}
|
|
118
|
+
}).pipe(withNode(test)),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
Vitest.live('cleans up logged child process when interrupted', (test) =>
|
|
122
|
+
Effect.gen(function* () {
|
|
123
|
+
const workspace = process.env.WORKSPACE_ROOT!
|
|
124
|
+
const logsDir = path.join(workspace, 'tmp', 'cmd-tests', `timeout-${Date.now()}`)
|
|
125
|
+
|
|
126
|
+
const result = yield* cmd(['node', '-e', 'setTimeout(() => {}, 5000)'], {
|
|
127
|
+
logDir: logsDir,
|
|
128
|
+
stdout: 'pipe',
|
|
129
|
+
stderr: 'pipe',
|
|
130
|
+
}).pipe(Effect.timeoutOption(Duration.millis(200)))
|
|
131
|
+
|
|
132
|
+
expect(result._tag).toBe('None')
|
|
133
|
+
expect(fs.existsSync(path.join(logsDir, 'dev.log'))).toBe(true)
|
|
134
|
+
}).pipe(withNode(test)),
|
|
135
|
+
)
|
|
136
|
+
})
|