@livestore/utils-dev 0.4.0-dev.7 → 0.4.0-dev.8
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.json +1 -1
- package/dist/node/DockerComposeService/DockerComposeService.d.ts +14 -4
- package/dist/node/DockerComposeService/DockerComposeService.d.ts.map +1 -1
- package/dist/node/DockerComposeService/DockerComposeService.js +27 -10
- package/dist/node/DockerComposeService/DockerComposeService.js.map +1 -1
- package/dist/node/mod.d.ts +0 -2
- package/dist/node/mod.d.ts.map +1 -1
- package/dist/node/mod.js +0 -2
- package/dist/node/mod.js.map +1 -1
- package/dist/node-vitest/Vitest.d.ts +6 -6
- package/dist/node-vitest/Vitest.d.ts.map +1 -1
- package/dist/node-vitest/Vitest.js +2 -2
- package/dist/node-vitest/Vitest.js.map +1 -1
- package/dist/node-vitest/Vitest.test.js +12 -1
- package/dist/node-vitest/Vitest.test.js.map +1 -1
- package/dist/{node/WranglerDevServer → wrangler}/WranglerDevServer.d.ts +6 -6
- package/dist/wrangler/WranglerDevServer.d.ts.map +1 -0
- package/dist/wrangler/WranglerDevServer.js +90 -0
- package/dist/wrangler/WranglerDevServer.js.map +1 -0
- package/dist/wrangler/WranglerDevServer.test.d.ts.map +1 -0
- package/dist/wrangler/WranglerDevServer.test.js +77 -0
- package/dist/wrangler/WranglerDevServer.test.js.map +1 -0
- package/dist/wrangler/fixtures/cf-worker.d.ts.map +1 -0
- package/dist/wrangler/fixtures/cf-worker.js.map +1 -0
- package/dist/wrangler/mod.d.ts +2 -0
- package/dist/wrangler/mod.d.ts.map +1 -0
- package/dist/wrangler/mod.js +2 -0
- package/dist/wrangler/mod.js.map +1 -0
- package/package.json +7 -4
- package/src/node/DockerComposeService/DockerComposeService.ts +50 -10
- package/src/node/mod.ts +0 -7
- package/src/node-vitest/Vitest.test.ts +12 -1
- package/src/node-vitest/Vitest.ts +31 -19
- package/src/wrangler/WranglerDevServer.test.ts +133 -0
- package/src/wrangler/WranglerDevServer.ts +180 -0
- package/src/wrangler/mod.ts +6 -0
- package/dist/node/WranglerDevServer/WranglerDevServer.d.ts.map +0 -1
- package/dist/node/WranglerDevServer/WranglerDevServer.js +0 -122
- package/dist/node/WranglerDevServer/WranglerDevServer.js.map +0 -1
- package/dist/node/WranglerDevServer/WranglerDevServer.test.d.ts.map +0 -1
- package/dist/node/WranglerDevServer/WranglerDevServer.test.js +0 -179
- package/dist/node/WranglerDevServer/WranglerDevServer.test.js.map +0 -1
- package/dist/node/WranglerDevServer/fixtures/cf-worker.d.ts.map +0 -1
- package/dist/node/WranglerDevServer/fixtures/cf-worker.js.map +0 -1
- package/dist/node/WranglerDevServer/process-tree-manager.d.ts +0 -55
- package/dist/node/WranglerDevServer/process-tree-manager.d.ts.map +0 -1
- package/dist/node/WranglerDevServer/process-tree-manager.js +0 -178
- package/dist/node/WranglerDevServer/process-tree-manager.js.map +0 -1
- package/dist/node/vitest-docker-compose-setup.d.ts +0 -32
- package/dist/node/vitest-docker-compose-setup.d.ts.map +0 -1
- package/dist/node/vitest-docker-compose-setup.js +0 -131
- package/dist/node/vitest-docker-compose-setup.js.map +0 -1
- package/dist/node/vitest-wrangler-setup.d.ts +0 -27
- package/dist/node/vitest-wrangler-setup.d.ts.map +0 -1
- package/dist/node/vitest-wrangler-setup.js +0 -96
- package/dist/node/vitest-wrangler-setup.js.map +0 -1
- package/dist/node-vitest/polyfill.d.ts +0 -2
- package/dist/node-vitest/polyfill.d.ts.map +0 -1
- package/dist/node-vitest/polyfill.js +0 -3
- package/dist/node-vitest/polyfill.js.map +0 -1
- package/src/node/WranglerDevServer/WranglerDevServer.test.ts +0 -266
- package/src/node/WranglerDevServer/WranglerDevServer.ts +0 -266
- package/src/node/WranglerDevServer/process-tree-manager.ts +0 -263
- /package/dist/{node/WranglerDevServer → wrangler}/WranglerDevServer.test.d.ts +0 -0
- /package/dist/{node/WranglerDevServer → wrangler}/fixtures/cf-worker.d.ts +0 -0
- /package/dist/{node/WranglerDevServer → wrangler}/fixtures/cf-worker.js +0 -0
- /package/src/{node/WranglerDevServer → wrangler}/fixtures/cf-worker.ts +0 -0
- /package/src/{node/WranglerDevServer → wrangler}/fixtures/wrangler.toml +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../../src/wrangler/mod.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,0BAA0B,EAC/B,KAAK,iBAAiB,EACtB,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,wBAAwB,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mod.js","sourceRoot":"","sources":["../../src/wrangler/mod.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,wBAAwB,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livestore/utils-dev",
|
|
3
|
-
"version": "0.4.0-dev.
|
|
3
|
+
"version": "0.4.0-dev.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"./src/node-vitest/global.ts",
|
|
@@ -8,18 +8,21 @@
|
|
|
8
8
|
],
|
|
9
9
|
"exports": {
|
|
10
10
|
"./node": "./dist/node/mod.js",
|
|
11
|
-
"./node-vitest": "./dist/node-vitest/mod.js"
|
|
11
|
+
"./node-vitest": "./dist/node-vitest/mod.js",
|
|
12
|
+
"./wrangler": "./dist/wrangler/mod.js"
|
|
12
13
|
},
|
|
13
14
|
"dependencies": {
|
|
14
|
-
"@effect/opentelemetry": "0.56.
|
|
15
|
+
"@effect/opentelemetry": "0.56.6",
|
|
15
16
|
"@effect/vitest": "0.25.1",
|
|
17
|
+
"@iarna/toml": "2.2.5",
|
|
16
18
|
"@opentelemetry/api": "1.9.0",
|
|
17
19
|
"@opentelemetry/exporter-metrics-otlp-http": "0.203.0",
|
|
18
20
|
"@opentelemetry/exporter-trace-otlp-http": "0.203.0",
|
|
19
21
|
"@opentelemetry/sdk-metrics": "2.0.1",
|
|
20
22
|
"@opentelemetry/sdk-trace-base": "2.0.1",
|
|
21
23
|
"@opentelemetry/sdk-trace-node": "2.0.1",
|
|
22
|
-
"
|
|
24
|
+
"wrangler": "4.32.0",
|
|
25
|
+
"@livestore/utils": "0.4.0-dev.8"
|
|
23
26
|
},
|
|
24
27
|
"devDependencies": {},
|
|
25
28
|
"files": [
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
|
|
14
14
|
export class DockerComposeError extends Schema.TaggedError<DockerComposeError>()('DockerComposeError', {
|
|
15
15
|
cause: Schema.Defect,
|
|
16
|
-
|
|
16
|
+
note: Schema.String,
|
|
17
17
|
}) {}
|
|
18
18
|
|
|
19
19
|
export interface DockerComposeArgs {
|
|
@@ -43,6 +43,11 @@ export interface DockerComposeOperations {
|
|
|
43
43
|
options?: StartOptions,
|
|
44
44
|
) => Effect.Effect<void, DockerComposeError | PlatformError.PlatformError, Scope.Scope>
|
|
45
45
|
readonly stop: Effect.Effect<void, DockerComposeError | PlatformError.PlatformError>
|
|
46
|
+
readonly down: (options?: {
|
|
47
|
+
readonly env?: Record<string, string>
|
|
48
|
+
readonly volumes?: boolean
|
|
49
|
+
readonly removeOrphans?: boolean
|
|
50
|
+
}) => Effect.Effect<void, DockerComposeError | PlatformError.PlatformError>
|
|
46
51
|
readonly logs: (
|
|
47
52
|
options?: LogsOptions,
|
|
48
53
|
) => Stream.Stream<string, DockerComposeError | PlatformError.PlatformError, Scope.Scope>
|
|
@@ -67,7 +72,7 @@ export class DockerComposeService extends Effect.Service<DockerComposeService>()
|
|
|
67
72
|
: Effect.fail(
|
|
68
73
|
new DockerComposeError({
|
|
69
74
|
cause: new Error(`Docker compose pull failed with exit code ${exitCode}`),
|
|
70
|
-
|
|
75
|
+
note: `Docker compose pull failed with exit code ${exitCode}`,
|
|
71
76
|
}),
|
|
72
77
|
),
|
|
73
78
|
),
|
|
@@ -89,12 +94,14 @@ export class DockerComposeService extends Effect.Service<DockerComposeService>()
|
|
|
89
94
|
const command = yield* Command.make(baseArgs[0]!, ...baseArgs.slice(1)).pipe(
|
|
90
95
|
Command.workingDirectory(cwd),
|
|
91
96
|
Command.env(options.env ?? {}),
|
|
97
|
+
Command.stderr('inherit'),
|
|
98
|
+
Command.stdout('inherit'),
|
|
92
99
|
Command.start,
|
|
93
100
|
Effect.catchAll((cause) =>
|
|
94
101
|
Effect.fail(
|
|
95
102
|
new DockerComposeError({
|
|
96
103
|
cause,
|
|
97
|
-
|
|
104
|
+
note: `Failed to start Docker Compose services in ${cwd}`,
|
|
98
105
|
}),
|
|
99
106
|
),
|
|
100
107
|
),
|
|
@@ -103,13 +110,13 @@ export class DockerComposeService extends Effect.Service<DockerComposeService>()
|
|
|
103
110
|
|
|
104
111
|
// Wait for command completion
|
|
105
112
|
yield* command.exitCode.pipe(
|
|
106
|
-
Effect.flatMap((exitCode
|
|
113
|
+
Effect.flatMap((exitCode) =>
|
|
107
114
|
exitCode === 0
|
|
108
115
|
? Effect.void
|
|
109
116
|
: Effect.fail(
|
|
110
117
|
new DockerComposeError({
|
|
111
118
|
cause: new Error(`Docker compose exited with code ${exitCode}`),
|
|
112
|
-
|
|
119
|
+
note: `Docker Compose failed to start with exit code ${exitCode}. Env: ${JSON.stringify(options.env)}`,
|
|
113
120
|
}),
|
|
114
121
|
),
|
|
115
122
|
),
|
|
@@ -140,7 +147,7 @@ export class DockerComposeService extends Effect.Service<DockerComposeService>()
|
|
|
140
147
|
: Effect.fail(
|
|
141
148
|
new DockerComposeError({
|
|
142
149
|
cause: new Error(`Docker compose stop exited with code ${exitCode}`),
|
|
143
|
-
|
|
150
|
+
note: `Failed to stop Docker Compose services`,
|
|
144
151
|
}),
|
|
145
152
|
),
|
|
146
153
|
),
|
|
@@ -167,7 +174,7 @@ export class DockerComposeService extends Effect.Service<DockerComposeService>()
|
|
|
167
174
|
Effect.fail(
|
|
168
175
|
new DockerComposeError({
|
|
169
176
|
cause,
|
|
170
|
-
|
|
177
|
+
note: `Failed to read Docker Compose logs in ${cwd}`,
|
|
171
178
|
}),
|
|
172
179
|
),
|
|
173
180
|
),
|
|
@@ -181,13 +188,46 @@ export class DockerComposeService extends Effect.Service<DockerComposeService>()
|
|
|
181
188
|
(cause) =>
|
|
182
189
|
new DockerComposeError({
|
|
183
190
|
cause,
|
|
184
|
-
|
|
191
|
+
note: `Error reading Docker Compose logs in ${cwd}`,
|
|
185
192
|
}),
|
|
186
193
|
),
|
|
187
194
|
)
|
|
188
195
|
}).pipe(Stream.unwrapScoped)
|
|
189
196
|
|
|
190
|
-
|
|
197
|
+
const down = (options?: {
|
|
198
|
+
readonly env?: Record<string, string>
|
|
199
|
+
readonly volumes?: boolean
|
|
200
|
+
readonly removeOrphans?: boolean
|
|
201
|
+
}) =>
|
|
202
|
+
Effect.gen(function* () {
|
|
203
|
+
yield* Effect.log(`Tearing down Docker Compose services in ${cwd}`)
|
|
204
|
+
|
|
205
|
+
const baseArgs = ['docker', 'compose', 'down']
|
|
206
|
+
if (options?.volumes) baseArgs.push('-v')
|
|
207
|
+
if (options?.removeOrphans) baseArgs.push('--remove-orphans')
|
|
208
|
+
if (serviceName) baseArgs.push(serviceName)
|
|
209
|
+
|
|
210
|
+
yield* Command.make(baseArgs[0]!, ...baseArgs.slice(1)).pipe(
|
|
211
|
+
Command.workingDirectory(cwd),
|
|
212
|
+
Command.env(options?.env ?? {}),
|
|
213
|
+
Command.exitCode,
|
|
214
|
+
Effect.flatMap((exitCode: number) =>
|
|
215
|
+
exitCode === 0
|
|
216
|
+
? Effect.void
|
|
217
|
+
: Effect.fail(
|
|
218
|
+
new DockerComposeError({
|
|
219
|
+
cause: new Error(`Docker compose down exited with code ${exitCode}`),
|
|
220
|
+
note: `Failed to tear down Docker Compose services`,
|
|
221
|
+
}),
|
|
222
|
+
),
|
|
223
|
+
),
|
|
224
|
+
Effect.provide(commandExecutorContext),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
yield* Effect.log(`Docker Compose services torn down successfully`)
|
|
228
|
+
}).pipe(Effect.withSpan('downDockerCompose'))
|
|
229
|
+
|
|
230
|
+
return { pull, start, stop, down, logs }
|
|
191
231
|
}),
|
|
192
232
|
}) {}
|
|
193
233
|
|
|
@@ -219,7 +259,7 @@ const performHealthCheck = ({
|
|
|
219
259
|
Effect.fail(
|
|
220
260
|
new DockerComposeError({
|
|
221
261
|
cause: new Error('Health check timeout'),
|
|
222
|
-
|
|
262
|
+
note: `Health check failed for ${url} after ${Duration.toMillis(timeout)}ms`,
|
|
223
263
|
}),
|
|
224
264
|
),
|
|
225
265
|
),
|
package/src/node/mod.ts
CHANGED
|
@@ -24,13 +24,6 @@ export {
|
|
|
24
24
|
startDockerComposeServicesScoped,
|
|
25
25
|
} from './DockerComposeService/DockerComposeService.ts'
|
|
26
26
|
export * as FileLogger from './FileLogger.ts'
|
|
27
|
-
export * from './WranglerDevServer/process-tree-manager.ts'
|
|
28
|
-
export {
|
|
29
|
-
type StartWranglerDevServerArgs,
|
|
30
|
-
type WranglerDevServer,
|
|
31
|
-
WranglerDevServerError,
|
|
32
|
-
WranglerDevServerService,
|
|
33
|
-
} from './WranglerDevServer/WranglerDevServer.ts'
|
|
34
27
|
|
|
35
28
|
export const OtelLiveHttp = ({
|
|
36
29
|
serviceName,
|
|
@@ -96,6 +96,17 @@ Vitest.describe('Vitest.asProp', () => {
|
|
|
96
96
|
|
|
97
97
|
return
|
|
98
98
|
}),
|
|
99
|
-
{
|
|
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
|
+
},
|
|
100
111
|
)
|
|
101
112
|
})
|
|
@@ -20,25 +20,29 @@ export * from '@effect/vitest'
|
|
|
20
20
|
|
|
21
21
|
export const DEBUGGER_ACTIVE = Boolean(process.env.DEBUGGER_ACTIVE ?? inspector.url() !== undefined)
|
|
22
22
|
|
|
23
|
-
export const makeWithTestCtx: <
|
|
24
|
-
ctxParams: WithTestCtxParams<
|
|
25
|
-
) => (
|
|
26
|
-
|
|
27
|
-
) => <
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
23
|
+
export const makeWithTestCtx: <ROut = never, E1 = never, RIn = never>(
|
|
24
|
+
ctxParams: WithTestCtxParams<ROut, E1, RIn>,
|
|
25
|
+
) => (testContext: Vitest.TestContext) => <A, E, R>(
|
|
26
|
+
self: Effect.Effect<A, E, R>,
|
|
27
|
+
) => Effect.Effect<
|
|
28
|
+
A,
|
|
29
|
+
E | E1 | Cause.TimeoutException,
|
|
30
|
+
// Exclude dependencies provided by `withTestCtx` from the layer dependencies
|
|
31
|
+
| Exclude<RIn, OtelTracer.OtelTracer | Scope.Scope>
|
|
32
|
+
// Exclude dependencies provided by `withTestCtx` **and** dependencies produced
|
|
33
|
+
// by the layer from the effect dependencies
|
|
34
|
+
| Exclude<R, ROut | OtelTracer.OtelTracer | Scope.Scope>
|
|
35
|
+
> = (ctxParams) => (testContext: Vitest.TestContext) => withTestCtx(testContext, ctxParams)
|
|
36
|
+
|
|
37
|
+
export type WithTestCtxParams<ROut, E1, RIn> = {
|
|
34
38
|
suffix?: string
|
|
35
|
-
makeLayer?: (testContext: Vitest.TestContext) => Layer.Layer<
|
|
39
|
+
makeLayer?: (testContext: Vitest.TestContext) => Layer.Layer<ROut, E1, RIn | Scope.Scope>
|
|
36
40
|
timeout?: Duration.DurationInput
|
|
37
41
|
forceOtel?: boolean
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
export const withTestCtx =
|
|
41
|
-
<
|
|
45
|
+
<ROut = never, E1 = never, RIn = never>(
|
|
42
46
|
testContext: Vitest.TestContext,
|
|
43
47
|
{
|
|
44
48
|
suffix,
|
|
@@ -47,23 +51,31 @@ export const withTestCtx =
|
|
|
47
51
|
forceOtel = false,
|
|
48
52
|
}: {
|
|
49
53
|
suffix?: string
|
|
50
|
-
makeLayer?: (testContext: Vitest.TestContext) => Layer.Layer<
|
|
54
|
+
makeLayer?: (testContext: Vitest.TestContext) => Layer.Layer<ROut, E1, RIn>
|
|
51
55
|
timeout?: Duration.DurationInput
|
|
52
56
|
forceOtel?: boolean
|
|
53
57
|
} = {},
|
|
54
58
|
) =>
|
|
55
|
-
<A, E>(
|
|
56
|
-
self: Effect.Effect<A, E,
|
|
57
|
-
): Effect.Effect<
|
|
59
|
+
<A, E, R>(
|
|
60
|
+
self: Effect.Effect<A, E, R>,
|
|
61
|
+
): Effect.Effect<
|
|
62
|
+
A,
|
|
63
|
+
E | E1 | Cause.TimeoutException,
|
|
64
|
+
// Exclude dependencies provided internally from the provided layer's dependencies
|
|
65
|
+
| Exclude<RIn, OtelTracer.OtelTracer | Scope.Scope>
|
|
66
|
+
// Exclude dependencies provided internally **and** dependencies produced by the
|
|
67
|
+
// provided layer from the effect dependencies
|
|
68
|
+
| Exclude<R, ROut | OtelTracer.OtelTracer | Scope.Scope>
|
|
69
|
+
> => {
|
|
58
70
|
const spanName = `${testContext.task.suite?.name}:${testContext.task.name}${suffix ? `:${suffix}` : ''}`
|
|
59
|
-
const layer = makeLayer?.(testContext)
|
|
71
|
+
const layer = makeLayer?.(testContext) ?? Layer.empty
|
|
60
72
|
|
|
61
73
|
const otelLayer =
|
|
62
74
|
DEBUGGER_ACTIVE || forceOtel
|
|
63
75
|
? OtelLiveHttp({ rootSpanName: spanName, serviceName: 'vitest-runner', skipLogUrl: false })
|
|
64
76
|
: OtelLiveDummy
|
|
65
77
|
|
|
66
|
-
const combinedLayer =
|
|
78
|
+
const combinedLayer = layer.pipe(Layer.provideMerge(otelLayer))
|
|
67
79
|
|
|
68
80
|
return self.pipe(
|
|
69
81
|
DEBUGGER_ACTIVE
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { Effect, FetchHttpClient, Layer } from '@livestore/utils/effect'
|
|
2
|
+
import { getFreePort, PlatformNode } from '@livestore/utils/node'
|
|
3
|
+
import { Vitest } from '@livestore/utils-dev/node-vitest'
|
|
4
|
+
import { expect } from 'vitest'
|
|
5
|
+
import {
|
|
6
|
+
type StartWranglerDevServerArgs,
|
|
7
|
+
WranglerDevServerError,
|
|
8
|
+
WranglerDevServerService,
|
|
9
|
+
} from './WranglerDevServer.ts'
|
|
10
|
+
|
|
11
|
+
const testTimeout = 60_000
|
|
12
|
+
|
|
13
|
+
const WranglerDevServerTest = (args: Partial<StartWranglerDevServerArgs> = {}) =>
|
|
14
|
+
WranglerDevServerService.Default({
|
|
15
|
+
cwd: `${import.meta.dirname}/fixtures`,
|
|
16
|
+
...args,
|
|
17
|
+
}).pipe(Layer.provide(FetchHttpClient.layer))
|
|
18
|
+
|
|
19
|
+
Vitest.describe('WranglerDevServer', { timeout: testTimeout }, () => {
|
|
20
|
+
Vitest.describe('Basic Operations', () => {
|
|
21
|
+
const withBasicTest = (args: Partial<StartWranglerDevServerArgs> = {}) =>
|
|
22
|
+
Vitest.makeWithTestCtx({
|
|
23
|
+
timeout: testTimeout,
|
|
24
|
+
makeLayer: () => WranglerDevServerTest(args).pipe(Layer.provide(PlatformNode.NodeContext.layer)),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
Vitest.scopedLive('should start wrangler dev server and return port', (test) =>
|
|
28
|
+
Effect.gen(function* () {
|
|
29
|
+
const server = yield* WranglerDevServerService
|
|
30
|
+
|
|
31
|
+
expect(server.port).toBeGreaterThan(0)
|
|
32
|
+
expect(server.url).toMatch(/http:\/\/127.0.0.1:\d+/)
|
|
33
|
+
}).pipe(withBasicTest()(test)),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
Vitest.scopedLive('should use specified port when provided', (test) =>
|
|
37
|
+
Effect.andThen(getFreePort, (port) =>
|
|
38
|
+
Effect.gen(function* () {
|
|
39
|
+
const server = yield* WranglerDevServerService
|
|
40
|
+
|
|
41
|
+
expect(server.port).toBe(port)
|
|
42
|
+
expect(server.url).toBe(`http://127.0.0.1:${port}`)
|
|
43
|
+
}).pipe(withBasicTest({ preferredPort: port })(test)),
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
Vitest.describe('Error Handling', () => {
|
|
49
|
+
const withErrorTest = (args: Partial<StartWranglerDevServerArgs> = {}) =>
|
|
50
|
+
Vitest.makeWithTestCtx({
|
|
51
|
+
timeout: testTimeout,
|
|
52
|
+
makeLayer: () => WranglerDevServerTest(args).pipe(Layer.provide(PlatformNode.NodeContext.layer)),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
Vitest.scopedLive('should handle missing wrangler.toml but should timeout', (test) =>
|
|
56
|
+
Effect.gen(function* () {
|
|
57
|
+
const error = yield* WranglerDevServerService.pipe(
|
|
58
|
+
Effect.provide(
|
|
59
|
+
WranglerDevServerTest({
|
|
60
|
+
cwd: '/tmp',
|
|
61
|
+
wranglerConfigPath: '/dev/null',
|
|
62
|
+
connectTimeout: '500 millis',
|
|
63
|
+
}).pipe(Layer.provide(PlatformNode.NodeContext.layer)),
|
|
64
|
+
),
|
|
65
|
+
Effect.flip,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
expect(error).toBeInstanceOf(WranglerDevServerError)
|
|
69
|
+
}).pipe(Vitest.withTestCtx(test)),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
Vitest.scopedLive('should handle invalid working directory', (test) =>
|
|
73
|
+
Effect.gen(function* () {
|
|
74
|
+
const result = yield* WranglerDevServerService.pipe(
|
|
75
|
+
Effect.provide(
|
|
76
|
+
WranglerDevServerTest({
|
|
77
|
+
cwd: '/completely/nonexistent/directory',
|
|
78
|
+
}).pipe(Layer.provide(PlatformNode.NodeContext.layer)),
|
|
79
|
+
),
|
|
80
|
+
Effect.either,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
expect(result._tag).toBe('Left')
|
|
84
|
+
if (result._tag === 'Left') {
|
|
85
|
+
expect(result.left).toBeInstanceOf(WranglerDevServerError)
|
|
86
|
+
}
|
|
87
|
+
}).pipe(Vitest.withTestCtx(test)),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
Vitest.scopedLive('should timeout if server fails to start', (test) =>
|
|
91
|
+
Effect.gen(function* () {
|
|
92
|
+
// Create a command that will never output "Ready on"
|
|
93
|
+
const result = yield* WranglerDevServerService.pipe(
|
|
94
|
+
// Override the timeout for this test to be shorter
|
|
95
|
+
Effect.timeout('5 seconds'),
|
|
96
|
+
Effect.either,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
// This might succeed or fail depending on actual wrangler behavior
|
|
100
|
+
// The main point is testing timeout functionality
|
|
101
|
+
expect(['Left', 'Right']).toContain(result._tag)
|
|
102
|
+
}).pipe(withErrorTest()(test)),
|
|
103
|
+
)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
Vitest.describe('Service Pattern', () => {
|
|
107
|
+
const withServiceTest = (args: Partial<StartWranglerDevServerArgs> = {}) =>
|
|
108
|
+
Vitest.makeWithTestCtx({
|
|
109
|
+
timeout: testTimeout,
|
|
110
|
+
makeLayer: () => WranglerDevServerTest(args).pipe(Layer.provide(PlatformNode.NodeContext.layer)),
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
Vitest.scopedLive('should work with service pattern', (test) =>
|
|
114
|
+
Effect.gen(function* () {
|
|
115
|
+
const server = yield* WranglerDevServerService
|
|
116
|
+
|
|
117
|
+
expect(server.port).toBeGreaterThan(0)
|
|
118
|
+
expect(server.url).toMatch(/http:\/\/127.0.0.1:\d+/)
|
|
119
|
+
}).pipe(withServiceTest()(test)),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
Vitest.scopedLive('should work with custom port via service', (test) =>
|
|
123
|
+
Effect.andThen(getFreePort, (port) =>
|
|
124
|
+
Effect.gen(function* () {
|
|
125
|
+
const server = yield* WranglerDevServerService
|
|
126
|
+
|
|
127
|
+
expect(server.port).toBe(port)
|
|
128
|
+
expect(server.url).toBe(`http://127.0.0.1:${port}`)
|
|
129
|
+
}).pipe(withServiceTest({ preferredPort: port })(test)),
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
})
|
|
133
|
+
})
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import * as path from 'node:path'
|
|
2
|
+
import * as Toml from '@iarna/toml'
|
|
3
|
+
import { IS_CI } from '@livestore/utils'
|
|
4
|
+
import { Cause, Duration, Effect, FileSystem, HttpClient, Schedule, Schema } from '@livestore/utils/effect'
|
|
5
|
+
import { getFreePort } from '@livestore/utils/node'
|
|
6
|
+
import * as wrangler from 'wrangler'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Error type for WranglerDevServer operations
|
|
10
|
+
*/
|
|
11
|
+
export class WranglerDevServerError extends Schema.TaggedError<WranglerDevServerError>()('WranglerDevServerError', {
|
|
12
|
+
cause: Schema.Unknown,
|
|
13
|
+
message: Schema.String,
|
|
14
|
+
port: Schema.Number,
|
|
15
|
+
}) {}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* WranglerDevServer instance interface
|
|
19
|
+
*/
|
|
20
|
+
export interface WranglerDevServer {
|
|
21
|
+
readonly port: number
|
|
22
|
+
readonly url: string
|
|
23
|
+
// readonly processId: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Configuration for starting WranglerDevServer
|
|
28
|
+
*/
|
|
29
|
+
export interface StartWranglerDevServerArgs {
|
|
30
|
+
wranglerConfigPath?: string
|
|
31
|
+
cwd: string
|
|
32
|
+
/** The port to try first. The dev server may bind a different port if unavailable. */
|
|
33
|
+
preferredPort?: number
|
|
34
|
+
/** @default false */
|
|
35
|
+
showLogs?: boolean
|
|
36
|
+
inspectorPort?: number
|
|
37
|
+
connectTimeout?: Duration.DurationInput
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* WranglerDevServer as an Effect.Service.
|
|
42
|
+
*
|
|
43
|
+
* This service provides the WranglerDevServer properties and can be accessed
|
|
44
|
+
* directly to get port and url.
|
|
45
|
+
*
|
|
46
|
+
* TODO: Allow for config to be passed in via code instead of `wrangler.toml` file
|
|
47
|
+
* (would need to be placed in temporary file as wrangler only accepts files as config)
|
|
48
|
+
*/
|
|
49
|
+
export class WranglerDevServerService extends Effect.Service<WranglerDevServerService>()('WranglerDevServerService', {
|
|
50
|
+
scoped: (args: StartWranglerDevServerArgs) =>
|
|
51
|
+
Effect.gen(function* () {
|
|
52
|
+
const showLogs = args.showLogs ?? false
|
|
53
|
+
|
|
54
|
+
// Allocate preferred port (Wrangler may bind a different one if unavailable)
|
|
55
|
+
const preferredPort =
|
|
56
|
+
args.preferredPort ??
|
|
57
|
+
(yield* getFreePort.pipe(
|
|
58
|
+
Effect.mapError(
|
|
59
|
+
(cause) => new WranglerDevServerError({ cause, message: 'Failed to get free port', port: -1 }),
|
|
60
|
+
),
|
|
61
|
+
))
|
|
62
|
+
|
|
63
|
+
yield* Effect.annotateCurrentSpan({ preferredPort })
|
|
64
|
+
|
|
65
|
+
const configPath = path.resolve(args.wranglerConfigPath ?? path.join(args.cwd, 'wrangler.toml'))
|
|
66
|
+
|
|
67
|
+
const fs = yield* FileSystem.FileSystem
|
|
68
|
+
const configContent = yield* fs.readFileString(configPath)
|
|
69
|
+
const parsedConfig = yield* Effect.try(() => Toml.parse(configContent)).pipe(
|
|
70
|
+
Effect.andThen(Schema.decodeUnknown(Schema.Struct({ main: Schema.String }))),
|
|
71
|
+
Effect.mapError(
|
|
72
|
+
(error) => new WranglerDevServerError({ cause: error, message: 'Failed to parse wrangler config', port: -1 }),
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
const resolvedMainPath = yield* Effect.try(() => path.resolve(args.cwd, parsedConfig.main))
|
|
76
|
+
|
|
77
|
+
const devServer = yield* Effect.promise(() =>
|
|
78
|
+
wrangler.unstable_dev(resolvedMainPath, {
|
|
79
|
+
config: configPath,
|
|
80
|
+
port: preferredPort,
|
|
81
|
+
inspectorPort: args.inspectorPort ?? 0,
|
|
82
|
+
persistTo: path.join(args.cwd, '.wrangler/state'),
|
|
83
|
+
logLevel: showLogs ? 'info' : 'none',
|
|
84
|
+
experimental: {
|
|
85
|
+
disableExperimentalWarning: true,
|
|
86
|
+
},
|
|
87
|
+
}),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
yield* Effect.addFinalizer(
|
|
91
|
+
Effect.fn(
|
|
92
|
+
function* (exit) {
|
|
93
|
+
if (exit._tag === 'Failure' && Cause.isInterruptedOnly(exit.cause) === false) {
|
|
94
|
+
yield* Effect.logError('Closing wrangler dev server on failure', exit.cause)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
yield* Effect.tryPromise(async () => {
|
|
98
|
+
await devServer.stop()
|
|
99
|
+
// TODO investigate whether we need to wait until exit (see workers-sdk repo/talk to Cloudflare team)
|
|
100
|
+
// await devServer.waitUntilExit()
|
|
101
|
+
})
|
|
102
|
+
},
|
|
103
|
+
Effect.timeout('5 seconds'),
|
|
104
|
+
Effect.orDie,
|
|
105
|
+
Effect.tapCauseLogPretty,
|
|
106
|
+
Effect.withSpan('WranglerDevServerService:stopDevServer'),
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const actualPort = devServer.port
|
|
111
|
+
const actualHost = devServer.address
|
|
112
|
+
const url = `http://${actualHost}:${actualPort}`
|
|
113
|
+
|
|
114
|
+
// Use longer timeout in CI environments to account for slower startup times
|
|
115
|
+
const defaultTimeout = Duration.seconds(IS_CI ? 30 : 5)
|
|
116
|
+
|
|
117
|
+
yield* verifyHttpConnectivity({ url, showLogs, connectTimeout: args.connectTimeout ?? defaultTimeout })
|
|
118
|
+
|
|
119
|
+
if (showLogs) {
|
|
120
|
+
yield* Effect.logDebug(
|
|
121
|
+
`Wrangler dev server ready and accepting connections on port ${actualPort} (preferred: ${preferredPort})`,
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
port: actualPort,
|
|
127
|
+
url,
|
|
128
|
+
} satisfies WranglerDevServer
|
|
129
|
+
}).pipe(
|
|
130
|
+
Effect.mapError(
|
|
131
|
+
(error) =>
|
|
132
|
+
new WranglerDevServerError({ cause: error, message: 'Failed to start wrangler dev server', port: -1 }),
|
|
133
|
+
),
|
|
134
|
+
Effect.withSpan('WranglerDevServerService', {
|
|
135
|
+
attributes: { preferredPort: args.preferredPort ?? 'auto', cwd: args.cwd },
|
|
136
|
+
}),
|
|
137
|
+
),
|
|
138
|
+
}) {}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Verifies the server is actually accepting HTTP connections by making a test request
|
|
142
|
+
*/
|
|
143
|
+
const verifyHttpConnectivity = ({
|
|
144
|
+
url,
|
|
145
|
+
showLogs,
|
|
146
|
+
connectTimeout,
|
|
147
|
+
}: {
|
|
148
|
+
url: string
|
|
149
|
+
showLogs: boolean
|
|
150
|
+
connectTimeout: Duration.DurationInput
|
|
151
|
+
}): Effect.Effect<void, WranglerDevServerError, HttpClient.HttpClient> =>
|
|
152
|
+
Effect.gen(function* () {
|
|
153
|
+
const client = yield* HttpClient.HttpClient
|
|
154
|
+
|
|
155
|
+
if (showLogs) {
|
|
156
|
+
yield* Effect.logDebug(`Verifying HTTP connectivity to ${url}`)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Try to connect with retries using exponential backoff
|
|
160
|
+
yield* client.get(url).pipe(
|
|
161
|
+
Effect.retryOrElse(
|
|
162
|
+
Schedule.exponential('50 millis', 2).pipe(
|
|
163
|
+
Schedule.jittered,
|
|
164
|
+
Schedule.intersect(Schedule.elapsed.pipe(Schedule.whileOutput(Duration.lessThanOrEqualTo(connectTimeout)))),
|
|
165
|
+
Schedule.compose(Schedule.count),
|
|
166
|
+
),
|
|
167
|
+
(error, attemptCount) =>
|
|
168
|
+
Effect.fail(
|
|
169
|
+
new WranglerDevServerError({
|
|
170
|
+
cause: error,
|
|
171
|
+
message: `Failed to establish HTTP connection to Wrangler server at ${url} after ${attemptCount} attempts (timeout: ${Duration.toMillis(connectTimeout)}ms)`,
|
|
172
|
+
port: 0,
|
|
173
|
+
}),
|
|
174
|
+
),
|
|
175
|
+
),
|
|
176
|
+
Effect.tap(() => (showLogs ? Effect.logDebug(`HTTP connectivity verified for ${url}`) : Effect.void)),
|
|
177
|
+
Effect.asVoid,
|
|
178
|
+
Effect.withSpan('verifyHttpConnectivity'),
|
|
179
|
+
)
|
|
180
|
+
})
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"WranglerDevServer.d.ts","sourceRoot":"","sources":["../../../src/node/WranglerDevServer/WranglerDevServer.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,QAAQ,EACR,MAAM,EAEN,UAAU,EAGV,MAAM,EAEP,MAAM,yBAAyB,CAAA;;;;;;;;AAIhC;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,2BAI1C;CAAG;AAEL;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,cAAc,CAAC,EAAE,QAAQ,CAAC,aAAa,CAAA;CACxC;;4BAYgB,0BAA0B;;;;;;AAV3C;;;;;;;;GAQG;AACH,qBAAa,wBAAyB,SAAQ,6BA6I5C;CAAG"}
|