@livestore/utils-dev 0.0.0-snapshot-e5a7caabcf7660aefa55568a44ed0fe306d00597 → 0.0.0-snapshot-9d8807d2c51c95b4df3556744702cea55dc7ded3

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.
Files changed (82) hide show
  1. package/dist/.tsbuildinfo.json +1 -1
  2. package/dist/node/mod.d.ts +13 -5
  3. package/dist/node/mod.d.ts.map +1 -1
  4. package/dist/node/mod.js +42 -67
  5. package/dist/node/mod.js.map +1 -1
  6. package/dist/node-vitest/mod.d.ts +1 -2
  7. package/dist/node-vitest/mod.d.ts.map +1 -1
  8. package/dist/node-vitest/mod.js +1 -2
  9. package/dist/node-vitest/mod.js.map +1 -1
  10. package/dist/node-vitest/polyfill.d.ts +2 -0
  11. package/dist/node-vitest/polyfill.d.ts.map +1 -0
  12. package/dist/node-vitest/{global.js → polyfill.js} +1 -1
  13. package/dist/node-vitest/polyfill.js.map +1 -0
  14. package/package.json +22 -16
  15. package/src/node/mod.ts +82 -81
  16. package/src/node-vitest/mod.ts +1 -3
  17. package/dist/node/DockerComposeService/DockerComposeService.d.ts +0 -58
  18. package/dist/node/DockerComposeService/DockerComposeService.d.ts.map +0 -1
  19. package/dist/node/DockerComposeService/DockerComposeService.js +0 -144
  20. package/dist/node/DockerComposeService/DockerComposeService.js.map +0 -1
  21. package/dist/node/DockerComposeService/DockerComposeService.test.d.ts +0 -2
  22. package/dist/node/DockerComposeService/DockerComposeService.test.d.ts.map +0 -1
  23. package/dist/node/DockerComposeService/DockerComposeService.test.js +0 -64
  24. package/dist/node/DockerComposeService/DockerComposeService.test.js.map +0 -1
  25. package/dist/node/FileLogger.d.ts +0 -14
  26. package/dist/node/FileLogger.d.ts.map +0 -1
  27. package/dist/node/FileLogger.js +0 -151
  28. package/dist/node/FileLogger.js.map +0 -1
  29. package/dist/node/cmd-log.d.ts +0 -21
  30. package/dist/node/cmd-log.d.ts.map +0 -1
  31. package/dist/node/cmd-log.js +0 -50
  32. package/dist/node/cmd-log.js.map +0 -1
  33. package/dist/node/cmd.d.ts +0 -36
  34. package/dist/node/cmd.d.ts.map +0 -1
  35. package/dist/node/cmd.js +0 -234
  36. package/dist/node/cmd.js.map +0 -1
  37. package/dist/node/cmd.test.d.ts +0 -2
  38. package/dist/node/cmd.test.d.ts.map +0 -1
  39. package/dist/node/cmd.test.js +0 -101
  40. package/dist/node/cmd.test.js.map +0 -1
  41. package/dist/node-vitest/Vitest.d.ts +0 -52
  42. package/dist/node-vitest/Vitest.d.ts.map +0 -1
  43. package/dist/node-vitest/Vitest.js +0 -98
  44. package/dist/node-vitest/Vitest.js.map +0 -1
  45. package/dist/node-vitest/Vitest.test.d.ts +0 -2
  46. package/dist/node-vitest/Vitest.test.d.ts.map +0 -1
  47. package/dist/node-vitest/Vitest.test.js +0 -70
  48. package/dist/node-vitest/Vitest.test.js.map +0 -1
  49. package/dist/node-vitest/global.d.ts +0 -2
  50. package/dist/node-vitest/global.d.ts.map +0 -1
  51. package/dist/node-vitest/global.js.map +0 -1
  52. package/dist/wrangler/WranglerDevServer.d.ts +0 -52
  53. package/dist/wrangler/WranglerDevServer.d.ts.map +0 -1
  54. package/dist/wrangler/WranglerDevServer.js +0 -90
  55. package/dist/wrangler/WranglerDevServer.js.map +0 -1
  56. package/dist/wrangler/WranglerDevServer.test.d.ts +0 -2
  57. package/dist/wrangler/WranglerDevServer.test.d.ts.map +0 -1
  58. package/dist/wrangler/WranglerDevServer.test.js +0 -77
  59. package/dist/wrangler/WranglerDevServer.test.js.map +0 -1
  60. package/dist/wrangler/fixtures/cf-worker.d.ts +0 -8
  61. package/dist/wrangler/fixtures/cf-worker.d.ts.map +0 -1
  62. package/dist/wrangler/fixtures/cf-worker.js +0 -11
  63. package/dist/wrangler/fixtures/cf-worker.js.map +0 -1
  64. package/dist/wrangler/mod.d.ts +0 -2
  65. package/dist/wrangler/mod.d.ts.map +0 -1
  66. package/dist/wrangler/mod.js +0 -2
  67. package/dist/wrangler/mod.js.map +0 -1
  68. package/src/node/DockerComposeService/DockerComposeService.test.ts +0 -91
  69. package/src/node/DockerComposeService/DockerComposeService.ts +0 -328
  70. package/src/node/DockerComposeService/test-fixtures/docker-compose.yml +0 -4
  71. package/src/node/FileLogger.ts +0 -206
  72. package/src/node/cmd-log.ts +0 -92
  73. package/src/node/cmd.test.ts +0 -129
  74. package/src/node/cmd.ts +0 -419
  75. package/src/node-vitest/Vitest.test.ts +0 -112
  76. package/src/node-vitest/Vitest.ts +0 -238
  77. package/src/wrangler/WranglerDevServer.test.ts +0 -133
  78. package/src/wrangler/WranglerDevServer.ts +0 -180
  79. package/src/wrangler/fixtures/cf-worker.ts +0 -11
  80. package/src/wrangler/fixtures/wrangler.toml +0 -11
  81. package/src/wrangler/mod.ts +0 -6
  82. /package/src/node-vitest/{global.ts → polyfill.ts} +0 -0
@@ -1,129 +0,0 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
-
4
- import { CommandExecutor, Duration, Effect } from '@livestore/utils/effect'
5
- import { PlatformNode } from '@livestore/utils/node'
6
- import { Vitest } from '@livestore/utils-dev/node-vitest'
7
- import { expect } from 'vitest'
8
- import { cmd } from './cmd.ts'
9
-
10
- const withNode = Vitest.makeWithTestCtx({
11
- makeLayer: () => PlatformNode.NodeContext.layer,
12
- timeout: 20_000,
13
- })
14
-
15
- Vitest.describe('cmd helper', () => {
16
- const ansiRegex = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g')
17
-
18
- Vitest.scopedLive('runs tokenized string without shell', (test) =>
19
- Effect.gen(function* () {
20
- const exit = yield* cmd('printf ok')
21
- expect(exit).toBe(CommandExecutor.ExitCode(0))
22
- }).pipe(withNode(test)),
23
- )
24
-
25
- Vitest.scopedLive('runs array input', (test) =>
26
- Effect.gen(function* () {
27
- const exit = yield* cmd(['printf', 'ok'])
28
- expect(exit).toBe(CommandExecutor.ExitCode(0))
29
- }).pipe(withNode(test)),
30
- )
31
-
32
- Vitest.scopedLive('supports logging with archive + retention', (test) =>
33
- Effect.gen(function* () {
34
- const workspace = process.env.WORKSPACE_ROOT!
35
- const logsDir = path.join(workspace, 'tmp', 'cmd-tests', String(Date.now()))
36
-
37
- // first run
38
- const exit1 = yield* cmd('printf first', { logDir: logsDir })
39
- expect(exit1).toBe(CommandExecutor.ExitCode(0))
40
- const current = path.join(logsDir, 'dev.log')
41
- expect(fs.existsSync(current)).toBe(true)
42
- const firstLog = fs.readFileSync(current, 'utf8')
43
- const firstStdoutLines = firstLog.split('\n').filter((line) => line.includes('[stdout]'))
44
- expect(firstStdoutLines.length).toBeGreaterThan(0)
45
- for (const line of firstStdoutLines) {
46
- expect(line).toContain('[stdout] first')
47
- expect(line).toContain('INFO')
48
- expect(line).toContain('printf first')
49
- }
50
-
51
- // second run — archives previous
52
- const exit2 = yield* cmd('printf second', { logDir: logsDir })
53
- expect(exit2).toBe(CommandExecutor.ExitCode(0))
54
- const archiveDir = path.join(logsDir, 'archive')
55
- const archives = fs.readdirSync(archiveDir).filter((f) => f.endsWith('.log'))
56
- expect(archives.length).toBe(1)
57
- const archivedPath = path.join(archiveDir, archives[0]!)
58
- const archivedLog = fs.readFileSync(archivedPath, 'utf8')
59
- const archivedStdoutLines = archivedLog.split('\n').filter((line) => line.includes('[stdout]'))
60
- expect(archivedStdoutLines.length).toBeGreaterThan(0)
61
- for (const line of archivedStdoutLines) {
62
- expect(line).toContain('[stdout] first')
63
- }
64
-
65
- const secondLog = fs.readFileSync(current, 'utf8')
66
- const secondStdoutLines = secondLog.split('\n').filter((line) => line.includes('[stdout]'))
67
- expect(secondStdoutLines.length).toBeGreaterThan(0)
68
- for (const line of secondStdoutLines) {
69
- expect(line).toContain('[stdout] second')
70
- expect(line).toContain('INFO')
71
- }
72
-
73
- // generate many archives to exercise retention (keep 50)
74
- for (let i = 0; i < 60; i++) {
75
- // Use small unique payloads
76
- yield* cmd(['printf', String(i)], { logDir: logsDir })
77
- }
78
- const archivesAfter = fs.readdirSync(archiveDir).filter((f) => f.endsWith('.log'))
79
- expect(archivesAfter.length).toBeLessThanOrEqual(50)
80
- }).pipe(withNode(test)),
81
- )
82
-
83
- Vitest.scopedLive('streams stdout and stderr with logger formatting', (test) =>
84
- Effect.gen(function* () {
85
- const workspace = process.env.WORKSPACE_ROOT!
86
- const logsDir = path.join(workspace, 'tmp', 'cmd-tests', `format-${Date.now()}`)
87
-
88
- const exit = yield* cmd(['node', '-e', "console.log('out'); console.error('err')"], {
89
- logDir: logsDir,
90
- })
91
- expect(exit).toBe(CommandExecutor.ExitCode(0))
92
-
93
- const current = path.join(logsDir, 'dev.log')
94
- const logContent = fs.readFileSync(current, 'utf8')
95
- expect(logContent).toMatch(/\[stdout] out/)
96
- expect(logContent).toMatch(/\[stderr] err/)
97
-
98
- const relevantLines = logContent
99
- .split('\n')
100
- .map((line) => line.trim())
101
- .filter((line) => line.includes('[stdout]') || line.includes('[stderr]'))
102
-
103
- expect(relevantLines.length).toBeGreaterThanOrEqual(2)
104
-
105
- for (const line of relevantLines) {
106
- const stripped = line.replace(ansiRegex, '')
107
- expect(stripped.startsWith('[')).toBe(true)
108
- expect(stripped).toMatch(/(INFO|WARN)/)
109
- expect(stripped).toMatch(/\[(stdout|stderr)]/)
110
- }
111
- }).pipe(withNode(test)),
112
- )
113
-
114
- Vitest.scopedLive('cleans up logged child process when interrupted', (test) =>
115
- Effect.gen(function* () {
116
- const workspace = process.env.WORKSPACE_ROOT!
117
- const logsDir = path.join(workspace, 'tmp', 'cmd-tests', `timeout-${Date.now()}`)
118
-
119
- const result = yield* cmd(['node', '-e', 'setTimeout(() => {}, 5000)'], {
120
- logDir: logsDir,
121
- stdout: 'pipe',
122
- stderr: 'pipe',
123
- }).pipe(Effect.timeoutOption(Duration.millis(200)))
124
-
125
- expect(result._tag).toBe('None')
126
- expect(fs.existsSync(path.join(logsDir, 'dev.log'))).toBe(true)
127
- }).pipe(withNode(test)),
128
- )
129
- })
package/src/node/cmd.ts DELETED
@@ -1,419 +0,0 @@
1
- import fs from 'node:fs'
2
-
3
- import { isNotUndefined, shouldNeverHappen } from '@livestore/utils'
4
- import {
5
- Cause,
6
- Command,
7
- type CommandExecutor,
8
- Effect,
9
- Fiber,
10
- FiberId,
11
- FiberRefs,
12
- HashMap,
13
- identity,
14
- List,
15
- LogLevel,
16
- type PlatformError,
17
- Schema,
18
- Stream,
19
- } from '@livestore/utils/effect'
20
- import { applyLoggingToCommand } from './cmd-log.ts'
21
- import * as FileLogger from './FileLogger.ts'
22
-
23
- // Branded zero value so we can compare exit codes without touching internals.
24
- const SUCCESS_EXIT_CODE: CommandExecutor.ExitCode = 0 as CommandExecutor.ExitCode
25
-
26
- export const cmd: (
27
- commandInput: string | (string | undefined)[],
28
- options?:
29
- | {
30
- cwd?: string
31
- stderr?: 'inherit' | 'pipe'
32
- stdout?: 'inherit' | 'pipe'
33
- shell?: boolean
34
- env?: Record<string, string | undefined>
35
- /**
36
- * When provided, streams command output to terminal AND to a canonical log file (`${logDir}/dev.log`) in this directory.
37
- * Also archives the previous run to `${logDir}/archive/dev-<ISO>.log` and keeps only the latest 50 archives.
38
- */
39
- logDir?: string
40
- /** Optional basename for the canonical log file; defaults to 'dev.log' */
41
- logFileName?: string
42
- /** Optional number of archived logs to retain; defaults to 50 */
43
- logRetention?: number
44
- }
45
- | undefined,
46
- ) => Effect.Effect<CommandExecutor.ExitCode, PlatformError.PlatformError | CmdError, CommandExecutor.CommandExecutor> =
47
- Effect.fn('cmd')(function* (commandInput, options) {
48
- const cwd = options?.cwd ?? process.env.WORKSPACE_ROOT ?? shouldNeverHappen('WORKSPACE_ROOT is not set')
49
-
50
- const asArray = Array.isArray(commandInput)
51
- const parts = asArray ? (commandInput as (string | undefined)[]).filter(isNotUndefined) : undefined
52
- const [command, ...args] = asArray ? (parts as string[]) : (commandInput as string).split(' ')
53
-
54
- const debugEnvStr = Object.entries(options?.env ?? {})
55
- .map(([key, value]) => `${key}='${value}' `)
56
- .join('')
57
-
58
- const loggingOpts = {
59
- ...(options?.logDir ? { logDir: options.logDir } : {}),
60
- ...(options?.logFileName ? { logFileName: options.logFileName } : {}),
61
- ...(options?.logRetention ? { logRetention: options.logRetention } : {}),
62
- } as const
63
- const { input: finalInput, subshell: needsShell, logPath } = yield* applyLoggingToCommand(commandInput, loggingOpts)
64
-
65
- const stdoutMode = options?.stdout ?? 'inherit'
66
- const stderrMode = options?.stderr ?? 'inherit'
67
- const useShell = (options?.shell ? true : false) || needsShell
68
-
69
- const commandDebugStr =
70
- debugEnvStr + (Array.isArray(finalInput) ? (finalInput as string[]).join(' ') : (finalInput as string))
71
- const subshellStr = useShell ? ' (in subshell)' : ''
72
-
73
- yield* Effect.logDebug(`Running '${commandDebugStr}' in '${cwd}'${subshellStr}`)
74
- yield* Effect.annotateCurrentSpan({
75
- 'span.label': commandDebugStr,
76
- cwd,
77
- command,
78
- args,
79
- logDir: options?.logDir,
80
- })
81
-
82
- const baseArgs = {
83
- commandInput: finalInput,
84
- cwd,
85
- env: options?.env ?? {},
86
- stdoutMode,
87
- stderrMode,
88
- useShell,
89
- } as const
90
-
91
- const exitCode = yield* isNotUndefined(logPath)
92
- ? Effect.gen(function* () {
93
- yield* Effect.sync(() => console.log(`Logging output to ${logPath}`))
94
- return yield* runWithLogging({ ...baseArgs, logPath, threadName: commandDebugStr })
95
- })
96
- : runWithoutLogging(baseArgs)
97
-
98
- if (exitCode !== SUCCESS_EXIT_CODE) {
99
- return yield* Effect.fail(
100
- CmdError.make({
101
- command: command!,
102
- args,
103
- cwd,
104
- env: options?.env ?? {},
105
- stderr: stderrMode,
106
- }),
107
- )
108
- }
109
-
110
- return exitCode
111
- })
112
-
113
- export const cmdText: (
114
- commandInput: string | (string | undefined)[],
115
- options?: {
116
- cwd?: string
117
- stderr?: 'inherit' | 'pipe'
118
- runInShell?: boolean
119
- env?: Record<string, string | undefined>
120
- },
121
- ) => Effect.Effect<string, PlatformError.PlatformError, CommandExecutor.CommandExecutor> = Effect.fn('cmdText')(
122
- function* (commandInput, options) {
123
- const cwd = options?.cwd ?? process.env.WORKSPACE_ROOT ?? shouldNeverHappen('WORKSPACE_ROOT is not set')
124
- const [command, ...args] = Array.isArray(commandInput)
125
- ? commandInput.filter(isNotUndefined)
126
- : commandInput.split(' ')
127
- const debugEnvStr = Object.entries(options?.env ?? {})
128
- .map(([key, value]) => `${key}='${value}' `)
129
- .join('')
130
-
131
- const commandDebugStr = debugEnvStr + [command, ...args].join(' ')
132
- const subshellStr = options?.runInShell ? ' (in subshell)' : ''
133
-
134
- yield* Effect.logDebug(`Running '${commandDebugStr}' in '${cwd}'${subshellStr}`)
135
- yield* Effect.annotateCurrentSpan({ 'span.label': commandDebugStr, command, cwd })
136
-
137
- return yield* Command.make(command!, ...args).pipe(
138
- // inherit = Stream stderr to process.stderr, pipe = Stream stderr to process.stdout
139
- Command.stderr(options?.stderr ?? 'inherit'),
140
- Command.workingDirectory(cwd),
141
- options?.runInShell ? Command.runInShell(true) : identity,
142
- Command.env(options?.env ?? {}),
143
- Command.string,
144
- )
145
- },
146
- )
147
-
148
- export class CmdError extends Schema.TaggedError<CmdError>()('CmdError', {
149
- command: Schema.String,
150
- args: Schema.Array(Schema.String),
151
- cwd: Schema.String,
152
- env: Schema.Record({ key: Schema.String, value: Schema.String.pipe(Schema.UndefinedOr) }),
153
- stderr: Schema.Literal('inherit', 'pipe'),
154
- }) {}
155
-
156
- type TRunBaseArgs = {
157
- readonly commandInput: string | string[]
158
- readonly cwd: string
159
- readonly env: Record<string, string | undefined>
160
- readonly stdoutMode: 'inherit' | 'pipe'
161
- readonly stderrMode: 'inherit' | 'pipe'
162
- readonly useShell: boolean
163
- }
164
-
165
- const runWithoutLogging = ({ commandInput, cwd, env, stdoutMode, stderrMode, useShell }: TRunBaseArgs) =>
166
- buildCommand(commandInput, useShell).pipe(
167
- Command.stdin('inherit'),
168
- Command.stdout(stdoutMode),
169
- Command.stderr(stderrMode),
170
- Command.workingDirectory(cwd),
171
- useShell ? Command.runInShell(true) : identity,
172
- Command.env(env),
173
- Command.exitCode,
174
- )
175
-
176
- type TRunWithLoggingArgs = TRunBaseArgs & {
177
- readonly logPath: string
178
- readonly threadName: string
179
- }
180
-
181
- const runWithLogging = ({
182
- commandInput,
183
- cwd,
184
- env,
185
- stdoutMode,
186
- stderrMode,
187
- useShell,
188
- logPath,
189
- threadName,
190
- }: TRunWithLoggingArgs) =>
191
- // When logging is enabled we have to replace the `2>&1 | tee` pipeline the
192
- // shell used to give us. We now pipe both streams through Effect so we can
193
- // mirror to the terminal (only when requested) and append formatted entries
194
- // into the canonical log ourselves.
195
- Effect.scoped(
196
- Effect.gen(function* () {
197
- const envWithColor = env.FORCE_COLOR === undefined ? { ...env, FORCE_COLOR: '1' } : env
198
-
199
- const logFile = yield* Effect.acquireRelease(
200
- Effect.sync(() => fs.openSync(logPath, 'a', 0o666)),
201
- (fd) => Effect.sync(() => fs.closeSync(fd)),
202
- )
203
-
204
- const prettyLogger = FileLogger.prettyLoggerTty({
205
- colors: true,
206
- stderr: false,
207
- formatDate: (date) => `${FileLogger.defaultDateFormat(date)} ${threadName}`,
208
- })
209
-
210
- const appendLog = ({ channel, content }: { channel: 'stdout' | 'stderr'; content: string }) =>
211
- Effect.sync(() => {
212
- const formatted = prettyLogger.log({
213
- fiberId: FiberId.none,
214
- logLevel: channel === 'stdout' ? LogLevel.Info : LogLevel.Warning,
215
- message: [`[${channel}]${content.length > 0 ? ` ${content}` : ''}`],
216
- cause: Cause.empty,
217
- context: FiberRefs.empty(),
218
- spans: List.empty(),
219
- annotations: HashMap.empty(),
220
- date: new Date(),
221
- })
222
- fs.writeSync(logFile, formatted)
223
- })
224
-
225
- const command = buildCommand(commandInput, useShell).pipe(
226
- Command.stdin('inherit'),
227
- Command.stdout('pipe'),
228
- Command.stderr('pipe'),
229
- Command.workingDirectory(cwd),
230
- useShell ? Command.runInShell(true) : identity,
231
- Command.env(envWithColor),
232
- )
233
-
234
- // Acquire/start the command and make sure we kill it on interruption.
235
- const runningProcess = yield* Effect.acquireRelease(command.pipe(Command.start), (proc) =>
236
- proc.isRunning.pipe(
237
- Effect.flatMap((running) => (running ? proc.kill().pipe(Effect.catchAll(() => Effect.void)) : Effect.void)),
238
- Effect.ignore,
239
- ),
240
- )
241
-
242
- const stdoutHandler = makeStreamHandler({
243
- channel: 'stdout',
244
- ...(stdoutMode === 'inherit' ? { mirrorTarget: process.stdout } : {}),
245
- appendLog,
246
- })
247
- const stderrHandler = makeStreamHandler({
248
- channel: 'stderr',
249
- ...(stderrMode === 'inherit' ? { mirrorTarget: process.stderr } : {}),
250
- appendLog,
251
- })
252
-
253
- const stdoutFiber = yield* runningProcess.stdout.pipe(
254
- Stream.decodeText('utf8'),
255
- Stream.runForEach((chunk) => stdoutHandler.onChunk(chunk)),
256
- Effect.forkScoped,
257
- )
258
-
259
- const stderrFiber = yield* runningProcess.stderr.pipe(
260
- Stream.decodeText('utf8'),
261
- Stream.runForEach((chunk) => stderrHandler.onChunk(chunk)),
262
- Effect.forkScoped,
263
- )
264
-
265
- // Dump any buffered data and finish both stream fibers before we return.
266
- const flushOutputs = Effect.gen(function* () {
267
- const stillRunning = yield* runningProcess.isRunning.pipe(Effect.catchAll(() => Effect.succeed(false)))
268
- if (stillRunning) {
269
- yield* Effect.ignore(runningProcess.kill())
270
- }
271
- yield* Effect.ignore(Fiber.join(stdoutFiber))
272
- yield* Effect.ignore(Fiber.join(stderrFiber))
273
- yield* stdoutHandler.flush()
274
- yield* stderrHandler.flush()
275
- })
276
-
277
- const exitCode = yield* runningProcess.exitCode.pipe(Effect.ensuring(flushOutputs))
278
-
279
- return exitCode
280
- }),
281
- )
282
-
283
- const buildCommand = (input: string | string[], useShell: boolean) => {
284
- if (Array.isArray(input)) {
285
- const [c, ...a] = input
286
- return Command.make(c!, ...a)
287
- }
288
-
289
- if (useShell) {
290
- return Command.make(input)
291
- }
292
-
293
- const [c, ...a] = input.split(' ')
294
- return Command.make(c!, ...a)
295
- }
296
-
297
- type TLineTerminator = 'newline' | 'carriage-return' | 'none'
298
-
299
- type TStreamHandler = {
300
- readonly onChunk: (chunk: string) => Effect.Effect<void, never>
301
- readonly flush: () => Effect.Effect<void, never>
302
- }
303
-
304
- const makeStreamHandler = ({
305
- channel,
306
- mirrorTarget,
307
- appendLog,
308
- }: {
309
- readonly channel: 'stdout' | 'stderr'
310
- readonly mirrorTarget?: NodeJS.WriteStream
311
- readonly appendLog: (args: { channel: 'stdout' | 'stderr'; content: string }) => Effect.Effect<void, never>
312
- }): TStreamHandler => {
313
- let buffer = ''
314
-
315
- // Effect's FileLogger expects line-oriented messages, but the subprocess
316
- // gives us arbitrary UTF-8 chunks. We keep a tiny line splitter here so the
317
- // log and console stay readable (and consistent with the previous `tee`
318
- // behaviour).
319
- const emit = (content: string, terminator: TLineTerminator) =>
320
- emitSegment({
321
- channel,
322
- content,
323
- terminator,
324
- ...(mirrorTarget ? { mirrorTarget } : {}),
325
- appendLog,
326
- })
327
-
328
- const consumeBuffer = (): Effect.Effect<void, never> => {
329
- if (buffer.length === 0) return Effect.void
330
-
331
- const lastChar = buffer[buffer.length - 1]
332
- if (lastChar === '\r') {
333
- const line = buffer.slice(0, -1)
334
- buffer = ''
335
- return emit(line, 'carriage-return')
336
- }
337
-
338
- const line = buffer
339
- buffer = ''
340
- return line.length === 0 ? Effect.void : emit(line, 'none')
341
- }
342
-
343
- return {
344
- onChunk: (chunk) =>
345
- Effect.gen(function* () {
346
- buffer += chunk
347
- while (buffer.length > 0) {
348
- const newlineIndex = buffer.indexOf('\n')
349
- const carriageIndex = buffer.indexOf('\r')
350
-
351
- if (newlineIndex === -1 && carriageIndex === -1) {
352
- break
353
- }
354
-
355
- let index: number
356
- let terminator: TLineTerminator
357
- let skip = 1
358
-
359
- if (carriageIndex !== -1 && (newlineIndex === -1 || carriageIndex < newlineIndex)) {
360
- index = carriageIndex
361
- if (carriageIndex + 1 < buffer.length && buffer[carriageIndex + 1] === '\n') {
362
- skip = 2
363
- terminator = 'newline'
364
- } else {
365
- terminator = 'carriage-return'
366
- }
367
- } else {
368
- index = newlineIndex!
369
- terminator = 'newline'
370
- }
371
-
372
- const line = buffer.slice(0, index)
373
- buffer = buffer.slice(index + skip)
374
- yield* emit(line, terminator)
375
- }
376
- }),
377
- flush: () => consumeBuffer(),
378
- }
379
- }
380
-
381
- const emitSegment = ({
382
- channel,
383
- content,
384
- terminator,
385
- mirrorTarget,
386
- appendLog,
387
- }: {
388
- readonly channel: 'stdout' | 'stderr'
389
- readonly content: string
390
- readonly terminator: TLineTerminator
391
- readonly mirrorTarget?: NodeJS.WriteStream
392
- readonly appendLog: (args: { channel: 'stdout' | 'stderr'; content: string }) => Effect.Effect<void, never>
393
- }) =>
394
- Effect.gen(function* () {
395
- if (mirrorTarget) {
396
- yield* Effect.sync(() => mirrorSegment(mirrorTarget, content, terminator))
397
- }
398
-
399
- const contentForLog = terminator === 'carriage-return' ? `${content}\r` : content
400
-
401
- yield* appendLog({ channel, content: contentForLog })
402
- })
403
-
404
- const mirrorSegment = (target: NodeJS.WriteStream, content: string, terminator: TLineTerminator) => {
405
- switch (terminator) {
406
- case 'newline': {
407
- target.write(`${content}\n`)
408
- break
409
- }
410
- case 'carriage-return': {
411
- target.write(`${content}\r`)
412
- break
413
- }
414
- case 'none': {
415
- target.write(content)
416
- break
417
- }
418
- }
419
- }
@@ -1,112 +0,0 @@
1
- import { Effect, FastCheck } from '@livestore/utils/effect'
2
- import * as Vitest from './Vitest.ts'
3
-
4
- // Demonstrate enhanced asProp functionality with clear shrinking progress
5
- // This showcases the fix for the "Run 26/6" bug and accurate shrinking detection
6
-
7
- Vitest.describe('Vitest.asProp', () => {
8
- const IntArbitrary = FastCheck.integer({ min: 1, max: 100 })
9
-
10
- // Always-passing test - should only show initial phase
11
- Vitest.asProp(
12
- Vitest.scopedLive,
13
- 'always-pass test (shows only initial runs)',
14
- [IntArbitrary],
15
- (properties, _ctx, enhanced) =>
16
- Effect.gen(function* () {
17
- const [value] = properties
18
- if (value === undefined) {
19
- return yield* Effect.fail(new Error('Value is undefined'))
20
- }
21
-
22
- console.log(
23
- `✅ ALWAYS-PASS [${enhanced._tag.toUpperCase()}]: ` +
24
- (enhanced._tag === 'initial'
25
- ? `Run ${enhanced.runIndex + 1}/${enhanced.numRuns}`
26
- : `Shrink #${enhanced.shrinkAttempt} (finding minimal counterexample)`) +
27
- `, value=${value}, total=${enhanced.totalExecutions}`,
28
- )
29
-
30
- // This test always passes, so no shrinking will occur
31
- return
32
- }),
33
- { fastCheck: { numRuns: 4 } },
34
- )
35
-
36
- // Failing test - should show initial + shrinking phases
37
- let alreadyFailed = false
38
- Vitest.asProp(
39
- Vitest.scopedLive,
40
- 'failing test (shows initial runs + shrinking)',
41
- [IntArbitrary],
42
- (properties, ctx, enhanced) =>
43
- Effect.gen(function* () {
44
- const [value] = properties
45
- if (value === undefined) {
46
- return yield* Effect.fail(new Error('Value is undefined'))
47
- }
48
-
49
- const displayInfo =
50
- enhanced._tag === 'initial'
51
- ? `Run ${enhanced.runIndex + 1}/${enhanced.numRuns}`
52
- : `Shrink #${enhanced.shrinkAttempt} (finding minimal counterexample)`
53
-
54
- const status = value > 80 ? '💥' : '✅'
55
- console.log(
56
- `${status} FAILING-TEST [${enhanced._tag.toUpperCase()}]: ${displayInfo}, value=${value}, total=${enhanced.totalExecutions}`,
57
- )
58
-
59
- // Fail when value is greater than 80 to trigger shrinking
60
- if (value > 80) {
61
- alreadyFailed = true
62
- return yield* Effect.fail(new Error(`Value ${value} is too large (> 80)`))
63
- }
64
-
65
- if (alreadyFailed && enhanced._tag === 'shrinking') {
66
- ctx.skip("For the sake of this test, we don't want to fail but want to skip")
67
- return
68
- }
69
-
70
- return
71
- }),
72
- { fastCheck: { numRuns: 3 } },
73
- )
74
-
75
- // Test with endOnFailure: true - should not show shrinking
76
- Vitest.asProp(
77
- Vitest.scopedLive,
78
- 'failing test with endOnFailure (no shrinking)',
79
- [IntArbitrary],
80
- (properties, _ctx, enhanced) =>
81
- Effect.gen(function* () {
82
- const [value] = properties
83
- if (value === undefined) {
84
- return yield* Effect.fail(new Error('Value is undefined'))
85
- }
86
-
87
- console.log(
88
- `🚫 END-ON-FAILURE [${enhanced._tag.toUpperCase()}]: ` +
89
- `Run ${enhanced.runIndex + 1}/${enhanced.numRuns}, value=${value}, total=${enhanced.totalExecutions}`,
90
- )
91
-
92
- // This will fail but shrinking is disabled
93
- if (value > 50) {
94
- return yield* Effect.fail(new Error(`Value ${value} is too large (> 50) - but no shrinking!`))
95
- }
96
-
97
- return
98
- }),
99
- {
100
- fastCheck: {
101
- numRuns: 5,
102
- endOnFailure: true,
103
- // Provide explicit samples so the second run crosses >50. Randomly drawing five
104
- // integers has ~3% chance to stay ≤ 50, which would break the `fails: true`
105
- // expectation even though shrinking remains disabled. The examples keep the
106
- // demo readable while leaving the remaining runs to FastCheck.
107
- examples: [[5], [66]],
108
- },
109
- fails: true,
110
- },
111
- )
112
- })