@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/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
|
-
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
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.
|
|
42
|
+
} = {}): Layer.Layer<OtelTracer.OtelTracer | Tracer.Tracer | Tracer.ParentSpan> =>
|
|
43
43
|
Effect.gen(function* () {
|
|
44
44
|
const configRes = yield* Config.all({
|
|
45
|
-
exporterUrl: Config.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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))
|
|
58
|
+
return withTracerReference(RootSpanLive.pipe(Layer.provideMerge(OtelLiveDummy)))
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
const config = configRes.value
|
|
62
62
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
73
|
+
onEnd: skipLogUrl === true ? undefined : (span) => logTraceUiUrlForTraceId()(span.traceId),
|
|
94
74
|
parent: parentSpan,
|
|
95
75
|
})
|
|
96
76
|
|
|
97
|
-
const
|
|
77
|
+
const baseLayer = RootSpanLive.pipe(Layer.provideMerge(Layer.mergeAll(OtelLive, OtelTracerLive)))
|
|
98
78
|
|
|
99
|
-
|
|
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*
|
|
105
|
-
const currentSpan = yield*
|
|
106
|
-
|
|
107
|
-
const nodeTiming =
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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.
|
|
122
|
+
return withTracerReference(layer)
|
|
123
|
+
}).pipe(Layer.unwrap)
|
|
133
124
|
|
|
134
125
|
export const logTraceUiUrlForSpan = (printMsg?: (url: string) => string) => (span: otel.Span) =>
|
|
135
|
-
|
|
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 = (
|
|
143
|
+
export const getTracingBackendUrl = (traceIdOrSpan: string | otel.Span) =>
|
|
150
144
|
Effect.gen(function* () {
|
|
151
|
-
const endpoint = yield* Config.string('GRAFANA_ENDPOINT').pipe(Config.option
|
|
145
|
+
const endpoint = yield* Config.string('GRAFANA_ENDPOINT').pipe(Config.option)
|
|
152
146
|
if (endpoint._tag === 'None') return
|
|
153
147
|
|
|
154
|
-
const 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
|
|
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.
|
|
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*
|
|
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.
|
|
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*
|
|
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*
|
|
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.
|
|
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*
|
|
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*
|
|
118
|
+
return yield* new TestError({ message: `Value ${value} is too large (> 50) - but no shrinking!` })
|
|
95
119
|
}
|
|
96
120
|
|
|
97
121
|
return
|