@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.
Files changed (82) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/node/DockerCompose/DockerCompose.d.ts +52 -0
  3. package/dist/node/DockerCompose/DockerCompose.d.ts.map +1 -0
  4. package/dist/node/DockerCompose/DockerCompose.js +158 -0
  5. package/dist/node/DockerCompose/DockerCompose.js.map +1 -0
  6. package/dist/node/DockerCompose/DockerCompose.test.d.ts +2 -0
  7. package/dist/node/DockerCompose/DockerCompose.test.d.ts.map +1 -0
  8. package/dist/node/{DockerComposeService/DockerComposeService.test.js → DockerCompose/DockerCompose.test.js} +18 -18
  9. package/dist/node/DockerCompose/DockerCompose.test.js.map +1 -0
  10. package/dist/node/DockerCompose/mod.d.ts +2 -0
  11. package/dist/node/DockerCompose/mod.d.ts.map +1 -0
  12. package/dist/node/DockerCompose/mod.js +2 -0
  13. package/dist/node/DockerCompose/mod.js.map +1 -0
  14. package/dist/node/FileLogger.d.ts +15 -8
  15. package/dist/node/FileLogger.d.ts.map +1 -1
  16. package/dist/node/FileLogger.js +64 -80
  17. package/dist/node/FileLogger.js.map +1 -1
  18. package/dist/node/cmd-log.d.ts +21 -0
  19. package/dist/node/cmd-log.d.ts.map +1 -0
  20. package/dist/node/cmd-log.js +58 -0
  21. package/dist/node/cmd-log.js.map +1 -0
  22. package/dist/node/cmd.d.ts +20 -14
  23. package/dist/node/cmd.d.ts.map +1 -1
  24. package/dist/node/cmd.js +248 -33
  25. package/dist/node/cmd.js.map +1 -1
  26. package/dist/node/cmd.test.d.ts +2 -0
  27. package/dist/node/cmd.test.d.ts.map +1 -0
  28. package/dist/node/cmd.test.js +103 -0
  29. package/dist/node/cmd.test.js.map +1 -0
  30. package/dist/node/mod.d.ts +5 -5
  31. package/dist/node/mod.d.ts.map +1 -1
  32. package/dist/node/mod.js +114 -66
  33. package/dist/node/mod.js.map +1 -1
  34. package/dist/node/workspace.d.ts +22 -0
  35. package/dist/node/workspace.d.ts.map +1 -0
  36. package/dist/node/workspace.js +26 -0
  37. package/dist/node/workspace.js.map +1 -0
  38. package/dist/node-vitest/Vitest.d.ts +17 -8
  39. package/dist/node-vitest/Vitest.d.ts.map +1 -1
  40. package/dist/node-vitest/Vitest.js +16 -16
  41. package/dist/node-vitest/Vitest.js.map +1 -1
  42. package/dist/node-vitest/Vitest.test.d.ts +6 -0
  43. package/dist/node-vitest/Vitest.test.d.ts.map +1 -1
  44. package/dist/node-vitest/Vitest.test.js +25 -10
  45. package/dist/node-vitest/Vitest.test.js.map +1 -1
  46. package/dist/wrangler/WranglerDevServer.d.ts +32 -20
  47. package/dist/wrangler/WranglerDevServer.d.ts.map +1 -1
  48. package/dist/wrangler/WranglerDevServer.js +79 -60
  49. package/dist/wrangler/WranglerDevServer.js.map +1 -1
  50. package/dist/wrangler/WranglerDevServer.test.js +31 -31
  51. package/dist/wrangler/WranglerDevServer.test.js.map +1 -1
  52. package/dist/wrangler/mod.d.ts +1 -1
  53. package/dist/wrangler/mod.d.ts.map +1 -1
  54. package/dist/wrangler/mod.js +1 -1
  55. package/dist/wrangler/mod.js.map +1 -1
  56. package/package.json +56 -21
  57. package/src/node/{DockerComposeService/DockerComposeService.test.ts → DockerCompose/DockerCompose.test.ts} +23 -20
  58. package/src/node/DockerCompose/DockerCompose.ts +328 -0
  59. package/src/node/DockerCompose/mod.ts +1 -0
  60. package/src/node/DockerCompose/test-fixtures/docker-compose.yml +4 -0
  61. package/src/node/FileLogger.ts +91 -115
  62. package/src/node/cmd-log.ts +95 -0
  63. package/src/node/cmd.test.ts +136 -0
  64. package/src/node/cmd.ts +431 -82
  65. package/src/node/mod.ts +152 -96
  66. package/src/node/workspace.ts +45 -0
  67. package/src/node-vitest/Vitest.test.ts +34 -10
  68. package/src/node-vitest/Vitest.ts +60 -66
  69. package/src/wrangler/WranglerDevServer.test.ts +38 -40
  70. package/src/wrangler/WranglerDevServer.ts +165 -108
  71. package/src/wrangler/fixtures/wrangler.toml +1 -1
  72. package/src/wrangler/mod.ts +1 -6
  73. package/dist/.tsbuildinfo.json +0 -1
  74. package/dist/node/DockerComposeService/DockerComposeService.d.ts +0 -58
  75. package/dist/node/DockerComposeService/DockerComposeService.d.ts.map +0 -1
  76. package/dist/node/DockerComposeService/DockerComposeService.js +0 -144
  77. package/dist/node/DockerComposeService/DockerComposeService.js.map +0 -1
  78. package/dist/node/DockerComposeService/DockerComposeService.test.d.ts +0 -2
  79. package/dist/node/DockerComposeService/DockerComposeService.test.d.ts.map +0 -1
  80. package/dist/node/DockerComposeService/DockerComposeService.test.js.map +0 -1
  81. package/src/node/DockerComposeService/DockerComposeService.ts +0 -328
  82. package/src/node/DockerComposeService/test-fixtures/docker-compose.yml +0 -4
package/src/node/mod.ts CHANGED
@@ -1,29 +1,29 @@
1
1
  import { performance } from 'node:perf_hooks'
2
2
 
3
- import * as OtelNodeSdk from '@effect/opentelemetry/NodeSdk'
4
- import { IS_BUN, isNonEmptyString } from '@livestore/utils'
5
- import type { Tracer } from '@livestore/utils/effect'
6
- import { Config, Effect, FiberRef, Layer, LogLevel, OtelTracer } from '@livestore/utils/effect'
7
- import { OtelLiveDummy } from '@livestore/utils/node'
8
3
  import * as otel from '@opentelemetry/api'
9
- import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'
10
- import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
11
- import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'
12
- import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'
4
+
5
+ import { IS_BUN } from '@livestore/utils'
6
+ import {
7
+ type Tracer,
8
+ Config,
9
+ Context,
10
+ Effect,
11
+ Exit,
12
+ FetchHttpClient,
13
+ Layer,
14
+ Option,
15
+ OtelTracer,
16
+ Otlp,
17
+ Schema,
18
+ } from '@livestore/utils/effect'
19
+ import { OtelLiveDummy } from '@livestore/utils/node'
13
20
 
14
21
  export { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'
15
22
  export { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
16
23
  export * from './cmd.ts'
17
- export {
18
- type DockerComposeArgs,
19
- DockerComposeError,
20
- type DockerComposeOperations,
21
- DockerComposeService,
22
- type LogsOptions,
23
- type StartOptions,
24
- startDockerComposeServicesScoped,
25
- } from './DockerComposeService/DockerComposeService.ts'
24
+ export * from './DockerCompose/mod.ts'
26
25
  export * as FileLogger from './FileLogger.ts'
26
+ export * from './workspace.ts'
27
27
 
28
28
  export const OtelLiveHttp = ({
29
29
  serviceName,
@@ -39,105 +39,99 @@ export const OtelLiveHttp = ({
39
39
  rootSpanAttributes?: Record<string, unknown>
40
40
  skipLogUrl?: boolean
41
41
  traceNodeBootstrap?: boolean
42
- } = {}): Layer.Layer<OtelTracer.OtelTracer | Tracer.ParentSpan, never, never> =>
42
+ } = {}): Layer.Layer<OtelTracer.OtelTracer | Tracer.Tracer | Tracer.ParentSpan> =>
43
43
  Effect.gen(function* () {
44
44
  const configRes = yield* Config.all({
45
- exporterUrl: Config.string('OTEL_EXPORTER_OTLP_ENDPOINT').pipe(
46
- Config.validate({ message: 'OTEL_EXPORTER_OTLP_ENDPOINT must be set', validation: isNonEmptyString }),
47
- ),
48
- serviceName: serviceName
49
- ? Config.succeed(serviceName)
50
- : Config.string('OTEL_SERVICE_NAME').pipe(Config.withDefault('livestore-utils-dev')),
51
- rootSpanName: rootSpanName
52
- ? Config.succeed(rootSpanName)
53
- : Config.string('OTEL_ROOT_SPAN_NAME').pipe(Config.withDefault('RootSpan')),
45
+ exporterUrl: Config.nonEmptyString('OTEL_EXPORTER_OTLP_ENDPOINT'),
46
+ serviceName:
47
+ serviceName !== undefined
48
+ ? Config.succeed(serviceName)
49
+ : Config.string('OTEL_SERVICE_NAME').pipe(Config.withDefault('livestore-utils-dev')),
50
+ rootSpanName:
51
+ rootSpanName !== undefined
52
+ ? Config.succeed(rootSpanName)
53
+ : Config.string('OTEL_ROOT_SPAN_NAME').pipe(Config.withDefault('RootSpan')),
54
54
  }).pipe(Effect.option)
55
55
 
56
56
  if (configRes._tag === 'None') {
57
57
  const RootSpanLive = Layer.span('DummyRoot', {})
58
- return RootSpanLive.pipe(Layer.provideMerge(OtelLiveDummy)) as any
58
+ return withTracerReference(RootSpanLive.pipe(Layer.provideMerge(OtelLiveDummy)))
59
59
  }
60
60
 
61
61
  const config = configRes.value
62
62
 
63
- const resource = { serviceName: config.serviceName }
64
-
65
- const metricReader = new PeriodicExportingMetricReader({
66
- exporter: new OTLPMetricExporter({ url: `${config.exporterUrl}/v1/metrics` }),
67
- exportIntervalMillis: 1000,
68
- })
69
-
70
- const OtelLive = OtelNodeSdk.layer(() => ({
71
- resource,
72
- metricReader,
73
- spanProcessor: new BatchSpanProcessor(
74
- new OTLPTraceExporter({ url: `${config.exporterUrl}/v1/traces`, headers: {} }),
75
- { scheduledDelayMillis: 50 },
76
- ),
77
- })).pipe(
78
- // If an OpenTelemetry backend is not available, the `OtelNodeSdk` layer
79
- // will ignore the error when attempting to connect and emit a debug log
80
- // stating the reason for the error (in this case `ECONNREFUSED`). This
81
- // can cause problems for programs which rely on clean `stdout` (e.g.
82
- // command-line applications). To remedy this, the below code sets the
83
- // minimum log level `FiberRef` to `"None"` for the duration of the
84
- // `OtelNodeSdk`'s layer constructor.
85
- //
86
- // This can likely be removed when Livestore is migrated to the Effect
87
- // native Otlp exporters.
88
- Layer.locally(FiberRef.currentMinimumLogLevel, LogLevel.None),
89
- )
63
+ const OtelLive = Otlp.layerJson({
64
+ baseUrl: config.exporterUrl,
65
+ metricsExportInterval: 1000,
66
+ tracerExportInterval: 50,
67
+ resource: { serviceName: config.serviceName },
68
+ }).pipe(Layer.provide(FetchHttpClient.layer))
69
+ const OtelTracerLive = Layer.succeed(OtelTracer.OtelTracer, otel.trace.getTracer(config.serviceName))
90
70
 
91
71
  const RootSpanLive = Layer.span(config.rootSpanName, {
92
72
  attributes: { config, ...rootSpanAttributes },
93
- onEnd: skipLogUrl ? undefined : (span: any) => logTraceUiUrlForSpan()(span.span),
73
+ onEnd: skipLogUrl === true ? undefined : (span) => logTraceUiUrlForTraceId()(span.traceId),
94
74
  parent: parentSpan,
95
75
  })
96
76
 
97
- const layer = yield* Layer.memoize(RootSpanLive.pipe(Layer.provideMerge(OtelLive)))
77
+ const baseLayer = RootSpanLive.pipe(Layer.provideMerge(Layer.mergeAll(OtelLive, OtelTracerLive)))
98
78
 
99
- if (traceNodeBootstrap) {
79
+ const memoMap = yield* Layer.makeMemoMap
80
+ const layer = Layer.fromBuild((_, scope) => Layer.buildWithMemoMap(baseLayer, memoMap, scope))
81
+
82
+ if (traceNodeBootstrap === true && IS_BUN === false) {
100
83
  /**
101
84
  * Create a span representing the Node.js bootstrap duration.
85
+ * Note: Skipped in Bun since performance.nodeTiming is not properly supported.
102
86
  */
103
87
  yield* Effect.gen(function* () {
104
- const tracer = yield* OtelTracer.OtelTracer
105
- const currentSpan = yield* OtelTracer.currentOtelSpan
106
-
107
- const nodeTiming = performance.nodeTiming
108
-
109
- // TODO get rid of this workaround for Bun once Bun properly supports performance.nodeTiming
110
- const startTime = IS_BUN ? nodeTiming.startTime : performance.timeOrigin + nodeTiming.nodeStart
111
-
112
- const bootSpan = tracer.startSpan(
113
- 'node-bootstrap',
114
- {
115
- startTime: nodeTiming.nodeStart,
116
- attributes: {
117
- 'node.timing.nodeStart': nodeTiming.nodeStart,
118
- 'node.timing.environment': nodeTiming.environment,
119
- 'node.timing.bootstrapComplete': nodeTiming.bootstrapComplete,
120
- 'node.timing.loopStart': nodeTiming.loopStart,
121
- 'node.timing.duration': nodeTiming.duration,
122
- },
123
- },
124
- otel.trace.setSpanContext(otel.context.active(), currentSpan.spanContext()),
125
- )
126
-
127
- bootSpan.end(startTime + nodeTiming.duration)
88
+ const tracer = yield* Effect.tracer
89
+ const currentSpan = yield* Effect.currentSpan
90
+
91
+ const { nodeTiming, endAbs, durationAttr } = computeBootstrapTiming()
92
+
93
+ const bootSpan = tracer.span({
94
+ name: 'node-bootstrap',
95
+ parent: Option.some(currentSpan),
96
+ annotations: Context.empty(),
97
+ links: [],
98
+ startTime: millisToNanos(
99
+ IS_BUN === true && typeof nodeTiming.nodeStart === 'number'
100
+ ? nodeTiming.nodeStart
101
+ : performance.timeOrigin + nodeTiming.nodeStart,
102
+ ),
103
+ kind: 'internal',
104
+ root: false,
105
+ sampled: true,
106
+ })
107
+
108
+ for (const [key, value] of Object.entries({
109
+ 'node.timing.nodeStart': nodeTiming.nodeStart,
110
+ 'node.timing.environment': nodeTiming.environment,
111
+ 'node.timing.bootstrapComplete': nodeTiming.bootstrapComplete,
112
+ 'node.timing.loopStart': nodeTiming.loopStart,
113
+ 'node.timing.duration': durationAttr,
114
+ })) {
115
+ if (value !== undefined) bootSpan.attribute(key, value)
116
+ }
117
+
118
+ bootSpan.end(millisToNanos(endAbs), Exit.void)
128
119
  }).pipe(Effect.provide(layer), Effect.orDie)
129
120
  }
130
121
 
131
- return layer
132
- }).pipe(Layer.unwrapScoped) as any
122
+ return withTracerReference(layer)
123
+ }).pipe(Layer.unwrap)
133
124
 
134
125
  export const logTraceUiUrlForSpan = (printMsg?: (url: string) => string) => (span: otel.Span) =>
135
- getTracingBackendUrl(span).pipe(
126
+ logTraceUiUrlForTraceId(printMsg)(span.spanContext().traceId)
127
+
128
+ const logTraceUiUrlForTraceId = (printMsg?: (url: string) => string) => (traceId: string) =>
129
+ getTracingBackendUrl(traceId).pipe(
136
130
  Effect.tap((url) => {
137
131
  if (url === undefined) {
138
132
  return Effect.logWarning('No tracing backend url found')
139
133
  } else {
140
- if (printMsg) {
134
+ if (printMsg !== undefined) {
141
135
  return Effect.log(printMsg(url))
142
136
  } else {
143
137
  return Effect.log(`Trace URL: ${url}`)
@@ -146,25 +140,87 @@ export const logTraceUiUrlForSpan = (printMsg?: (url: string) => string) => (spa
146
140
  }),
147
141
  )
148
142
 
149
- export const getTracingBackendUrl = (span: otel.Span) =>
143
+ export const getTracingBackendUrl = (traceIdOrSpan: string | otel.Span) =>
150
144
  Effect.gen(function* () {
151
- const endpoint = yield* Config.string('GRAFANA_ENDPOINT').pipe(Config.option, Effect.orDie)
145
+ const endpoint = yield* Config.string('GRAFANA_ENDPOINT').pipe(Config.option)
152
146
  if (endpoint._tag === 'None') return
153
147
 
154
- const traceId = span.spanContext().traceId
148
+ const traceId = typeof traceIdOrSpan === 'string' ? traceIdOrSpan : traceIdOrSpan.spanContext().traceId
155
149
 
156
150
  // Grafana + Tempo
157
151
 
158
152
  const grafanaEndpoint = endpoint.value
153
+ const left = yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))({
154
+ datasource: 'tempo',
155
+ queries: [{ query: traceId, queryType: 'traceql', refId: 'A' }],
156
+ range: { from: 'now-1h', to: 'now' },
157
+ })
159
158
  const searchParams = new URLSearchParams({
160
159
  orgId: '1',
161
- left: JSON.stringify({
162
- datasource: 'tempo',
163
- queries: [{ query: traceId, queryType: 'traceql', refId: 'A' }],
164
- range: { from: 'now-1h', to: 'now' },
165
- }),
160
+ left,
166
161
  })
167
162
 
168
163
  // TODO make dynamic via env var
169
164
  return `${grafanaEndpoint}/explore?${searchParams.toString()}`
170
- })
165
+ }).pipe(Effect.orDie)
166
+
167
+ /**
168
+ * Compute absolute start/end timestamps for the Node.js bootstrap span in a
169
+ * way that works in both Node and Bun.
170
+ *
171
+ * Context: Bun's perf_hooks PerformanceNodeTiming currently throws when
172
+ * accessing standard PerformanceEntry getters like `startTime` and
173
+ * `duration`, and some fields differ in semantics (e.g. `nodeStart` appears
174
+ * as an epoch timestamp rather than an offset). See:
175
+ * https://github.com/oven-sh/bun/issues/23041
176
+ *
177
+ * We therefore avoid the problematic getters and derive absolute timestamps
178
+ * using fields that exist in both runtimes.
179
+ *
180
+ * TODO: Simplify to a single, non-branching computation once the Bun issue
181
+ * above is fixed and Bun matches Node's semantics for PerformanceNodeTiming.
182
+ */
183
+ const computeBootstrapTiming = () => {
184
+ const nodeTiming = performance.nodeTiming
185
+
186
+ // Absolute start time in ms since epoch.
187
+ const startAbs =
188
+ IS_BUN === true
189
+ ? typeof nodeTiming.nodeStart === 'number'
190
+ ? nodeTiming.nodeStart
191
+ : performance.timeOrigin
192
+ : performance.timeOrigin + nodeTiming.nodeStart
193
+
194
+ // Absolute end time.
195
+ const endAbs =
196
+ IS_BUN === true
197
+ ? (() => {
198
+ const { loopStart, bootstrapComplete } = nodeTiming
199
+ if (typeof loopStart === 'number' && loopStart > 0) return startAbs + loopStart
200
+ if (typeof bootstrapComplete === 'number' && bootstrapComplete >= startAbs) return bootstrapComplete
201
+ return startAbs + 1
202
+ })()
203
+ : startAbs + nodeTiming.duration
204
+
205
+ // Duration attribute value for the span.
206
+ const durationAttr =
207
+ IS_BUN === true
208
+ ? (() => {
209
+ const { loopStart } = nodeTiming
210
+ return typeof loopStart === 'number' && loopStart > 0 ? loopStart : 0
211
+ })()
212
+ : nodeTiming.duration
213
+
214
+ return { nodeTiming, startAbs, endAbs, durationAttr } as const
215
+ }
216
+
217
+ const millisToNanos = (millis: number): bigint => BigInt(Math.round(millis * 1_000_000))
218
+
219
+ /**
220
+ * Effect v4 installs `Tracer.Tracer` through a `Context.Reference` fiber ref.
221
+ * The OTEL layers set that reference at runtime, but their layer output type is
222
+ * `never`, so we localize the public compatibility assertion here.
223
+ */
224
+ const withTracerReference = <ROut, E, RIn>(
225
+ layer: Layer.Layer<ROut, E, RIn>,
226
+ ): Layer.Layer<ROut | Tracer.Tracer, E, RIn> => layer as Layer.Layer<ROut | Tracer.Tracer, E, RIn>
@@ -0,0 +1,45 @@
1
+ import path from 'node:path'
2
+
3
+ import { shouldNeverHappen } from '@livestore/utils'
4
+ import { Context, Effect, Layer } from '@livestore/utils/effect'
5
+
6
+ export type WorkspaceInfo = string
7
+
8
+ /** Current working directory. */
9
+ export class CurrentWorkingDirectory extends Context.Service<CurrentWorkingDirectory, WorkspaceInfo>()(
10
+ 'CurrentWorkingDirectory',
11
+ ) {
12
+ /** Layer that captures the process cwd once. */
13
+ static live = Layer.effect(
14
+ CurrentWorkingDirectory,
15
+ Effect.sync(() => CurrentWorkingDirectory.of(process.cwd())),
16
+ )
17
+
18
+ /** Override CWD for tests or nested invocations. */
19
+ static fromPath = (cwd: string) => Layer.succeed(CurrentWorkingDirectory, CurrentWorkingDirectory.of(cwd))
20
+ }
21
+
22
+ /** Livestore workspace root (env required). */
23
+ export class LivestoreWorkspace extends Context.Service<LivestoreWorkspace, WorkspaceInfo>()('LivestoreWorkspace') {
24
+ /** Resolve from WORKSPACE_ROOT env. */
25
+ static live = Layer.effect(
26
+ LivestoreWorkspace,
27
+ Effect.sync(() => {
28
+ const root = process.env.WORKSPACE_ROOT ?? shouldNeverHappen('WORKSPACE_ROOT is not set')
29
+ return LivestoreWorkspace.of(root)
30
+ }),
31
+ )
32
+
33
+ /** Provide a fixed Livestore root. */
34
+ static fromPath = (root: string) => Layer.succeed(LivestoreWorkspace, LivestoreWorkspace.of(root))
35
+
36
+ /** Derive a CurrentWorkingDirectory layer from the Livestore workspace root (with optional subpath) */
37
+ static toCwd = (/** Relative path to the Livestore workspace root */ subPath?: string) =>
38
+ Layer.effect(
39
+ CurrentWorkingDirectory,
40
+ Effect.gen(function* () {
41
+ const root = yield* LivestoreWorkspace
42
+ return CurrentWorkingDirectory.of(path.join(root, subPath ?? ''))
43
+ }),
44
+ )
45
+ }
@@ -1,6 +1,11 @@
1
- import { Effect, FastCheck } from '@livestore/utils/effect'
1
+ import { Effect, FastCheck, Schema } from '@livestore/utils/effect'
2
+
2
3
  import * as Vitest from './Vitest.ts'
3
4
 
5
+ export class TestError extends Schema.TaggedError<TestError>()('TestError', {
6
+ message: Schema.String,
7
+ }) {}
8
+
4
9
  // Demonstrate enhanced asProp functionality with clear shrinking progress
5
10
  // This showcases the fix for the "Run 26/6" bug and accurate shrinking detection
6
11
 
@@ -9,14 +14,14 @@ Vitest.describe('Vitest.asProp', () => {
9
14
 
10
15
  // Always-passing test - should only show initial phase
11
16
  Vitest.asProp(
12
- Vitest.scopedLive,
17
+ Vitest.live,
13
18
  'always-pass test (shows only initial runs)',
14
19
  [IntArbitrary],
15
20
  (properties, _ctx, enhanced) =>
16
21
  Effect.gen(function* () {
17
22
  const [value] = properties
18
23
  if (value === undefined) {
19
- return yield* Effect.fail(new Error('Value is undefined'))
24
+ return yield* new TestError({ message: 'Value is undefined' })
20
25
  }
21
26
 
22
27
  console.log(
@@ -33,17 +38,36 @@ Vitest.describe('Vitest.asProp', () => {
33
38
  { fastCheck: { numRuns: 4 } },
34
39
  )
35
40
 
41
+ Vitest.asProp(
42
+ Vitest.live,
43
+ 'converts record-form schemas to arbitraries',
44
+ {
45
+ count: Schema.Int,
46
+ payload: Schema.Struct({
47
+ label: Schema.String,
48
+ note: Schema.optional(Schema.String),
49
+ }),
50
+ },
51
+ ({ count, payload }) =>
52
+ Effect.sync(() => {
53
+ Vitest.expect(Number.isInteger(count)).toBe(true)
54
+ Vitest.expect(typeof payload.label).toBe('string')
55
+ Vitest.expect(payload.note === undefined || typeof payload.note === 'string').toBe(true)
56
+ }),
57
+ { fastCheck: { numRuns: 10 } },
58
+ )
59
+
36
60
  // Failing test - should show initial + shrinking phases
37
61
  let alreadyFailed = false
38
62
  Vitest.asProp(
39
- Vitest.scopedLive,
63
+ Vitest.live,
40
64
  'failing test (shows initial runs + shrinking)',
41
65
  [IntArbitrary],
42
66
  (properties, ctx, enhanced) =>
43
67
  Effect.gen(function* () {
44
68
  const [value] = properties
45
69
  if (value === undefined) {
46
- return yield* Effect.fail(new Error('Value is undefined'))
70
+ return yield* new TestError({ message: 'Value is undefined' })
47
71
  }
48
72
 
49
73
  const displayInfo =
@@ -59,10 +83,10 @@ Vitest.describe('Vitest.asProp', () => {
59
83
  // Fail when value is greater than 80 to trigger shrinking
60
84
  if (value > 80) {
61
85
  alreadyFailed = true
62
- return yield* Effect.fail(new Error(`Value ${value} is too large (> 80)`))
86
+ return yield* new TestError({ message: `Value ${value} is too large (> 80)` })
63
87
  }
64
88
 
65
- if (alreadyFailed && enhanced._tag === 'shrinking') {
89
+ if (alreadyFailed === true && enhanced._tag === 'shrinking') {
66
90
  ctx.skip("For the sake of this test, we don't want to fail but want to skip")
67
91
  return
68
92
  }
@@ -74,14 +98,14 @@ Vitest.describe('Vitest.asProp', () => {
74
98
 
75
99
  // Test with endOnFailure: true - should not show shrinking
76
100
  Vitest.asProp(
77
- Vitest.scopedLive,
101
+ Vitest.live,
78
102
  'failing test with endOnFailure (no shrinking)',
79
103
  [IntArbitrary],
80
104
  (properties, _ctx, enhanced) =>
81
105
  Effect.gen(function* () {
82
106
  const [value] = properties
83
107
  if (value === undefined) {
84
- return yield* Effect.fail(new Error('Value is undefined'))
108
+ return yield* new TestError({ message: 'Value is undefined' })
85
109
  }
86
110
 
87
111
  console.log(
@@ -91,7 +115,7 @@ Vitest.describe('Vitest.asProp', () => {
91
115
 
92
116
  // This will fail but shrinking is disabled
93
117
  if (value > 50) {
94
- yield* Effect.fail(new Error(`Value ${value} is too large (> 50) - but no shrinking!`))
118
+ return yield* new TestError({ message: `Value ${value} is too large (> 50) - but no shrinking!` })
95
119
  }
96
120
 
97
121
  return