@livestore/utils-dev 0.4.0-dev.2 → 0.4.0-dev.20

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 (73) hide show
  1. package/dist/.tsbuildinfo.json +1 -1
  2. package/dist/node/DockerComposeService/DockerComposeService.d.ts +58 -0
  3. package/dist/node/DockerComposeService/DockerComposeService.d.ts.map +1 -0
  4. package/dist/node/DockerComposeService/DockerComposeService.js +144 -0
  5. package/dist/node/DockerComposeService/DockerComposeService.js.map +1 -0
  6. package/dist/node/DockerComposeService/DockerComposeService.test.d.ts +2 -0
  7. package/dist/node/DockerComposeService/DockerComposeService.test.d.ts.map +1 -0
  8. package/dist/node/DockerComposeService/DockerComposeService.test.js +64 -0
  9. package/dist/node/DockerComposeService/DockerComposeService.test.js.map +1 -0
  10. package/dist/node/cmd-log.d.ts +21 -0
  11. package/dist/node/cmd-log.d.ts.map +1 -0
  12. package/dist/node/cmd-log.js +47 -0
  13. package/dist/node/cmd-log.js.map +1 -0
  14. package/dist/node/cmd.d.ts +35 -0
  15. package/dist/node/cmd.d.ts.map +1 -0
  16. package/dist/node/cmd.js +235 -0
  17. package/dist/node/cmd.js.map +1 -0
  18. package/dist/node/cmd.test.d.ts +2 -0
  19. package/dist/node/cmd.test.d.ts.map +1 -0
  20. package/dist/node/cmd.test.js +102 -0
  21. package/dist/node/cmd.test.js.map +1 -0
  22. package/dist/node/mod.d.ts +5 -26
  23. package/dist/node/mod.d.ts.map +1 -1
  24. package/dist/node/mod.js +52 -59
  25. package/dist/node/mod.js.map +1 -1
  26. package/dist/node/workspace.d.ts +22 -0
  27. package/dist/node/workspace.d.ts.map +1 -0
  28. package/dist/node/workspace.js +26 -0
  29. package/dist/node/workspace.js.map +1 -0
  30. package/dist/node-vitest/Vitest.d.ts +41 -7
  31. package/dist/node-vitest/Vitest.d.ts.map +1 -1
  32. package/dist/node-vitest/Vitest.js +82 -5
  33. package/dist/node-vitest/Vitest.js.map +1 -1
  34. package/dist/node-vitest/Vitest.test.d.ts +2 -0
  35. package/dist/node-vitest/Vitest.test.d.ts.map +1 -0
  36. package/dist/node-vitest/Vitest.test.js +70 -0
  37. package/dist/node-vitest/Vitest.test.js.map +1 -0
  38. package/dist/wrangler/WranglerDevServer.d.ts +69 -0
  39. package/dist/wrangler/WranglerDevServer.d.ts.map +1 -0
  40. package/dist/wrangler/WranglerDevServer.js +103 -0
  41. package/dist/wrangler/WranglerDevServer.js.map +1 -0
  42. package/dist/wrangler/WranglerDevServer.test.d.ts +2 -0
  43. package/dist/wrangler/WranglerDevServer.test.d.ts.map +1 -0
  44. package/dist/wrangler/WranglerDevServer.test.js +77 -0
  45. package/dist/wrangler/WranglerDevServer.test.js.map +1 -0
  46. package/dist/wrangler/fixtures/cf-worker.d.ts +8 -0
  47. package/dist/wrangler/fixtures/cf-worker.d.ts.map +1 -0
  48. package/dist/wrangler/fixtures/cf-worker.js +11 -0
  49. package/dist/wrangler/fixtures/cf-worker.js.map +1 -0
  50. package/dist/wrangler/mod.d.ts +2 -0
  51. package/dist/wrangler/mod.d.ts.map +1 -0
  52. package/dist/wrangler/mod.js +2 -0
  53. package/dist/wrangler/mod.js.map +1 -0
  54. package/package.json +11 -10
  55. package/src/node/DockerComposeService/DockerComposeService.test.ts +91 -0
  56. package/src/node/DockerComposeService/DockerComposeService.ts +328 -0
  57. package/src/node/DockerComposeService/test-fixtures/docker-compose.yml +4 -0
  58. package/src/node/cmd-log.ts +87 -0
  59. package/src/node/cmd.test.ts +130 -0
  60. package/src/node/cmd.ts +420 -0
  61. package/src/node/mod.ts +63 -116
  62. package/src/node/workspace.ts +45 -0
  63. package/src/node-vitest/Vitest.test.ts +112 -0
  64. package/src/node-vitest/Vitest.ts +193 -17
  65. package/src/wrangler/WranglerDevServer.test.ts +133 -0
  66. package/src/wrangler/WranglerDevServer.ts +220 -0
  67. package/src/wrangler/fixtures/cf-worker.ts +11 -0
  68. package/src/wrangler/fixtures/wrangler.toml +11 -0
  69. package/src/wrangler/mod.ts +6 -0
  70. package/dist/node-vitest/polyfill.d.ts +0 -2
  71. package/dist/node-vitest/polyfill.d.ts.map +0 -1
  72. package/dist/node-vitest/polyfill.js +0 -3
  73. package/dist/node-vitest/polyfill.js.map +0 -1
@@ -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
+ readiness: { 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,220 @@
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
+ * Readiness and retry configuration for wrangler boot and HTTP health.
28
+ *
29
+ * Example: startupTimeout=20s, connectTimeout=5s, retrySchedule=recurs(1)
30
+ * - Give wrangler up to 20s to boot; if it succeeds, give the HTTP check up to 5s.
31
+ * - If wrangler fails/times out, retry boot once; each boot attempt gets its own 20s budget.
32
+ * connectTimeout should be shorter than startupTimeout because HTTP readiness should be fast after boot.
33
+ */
34
+ export interface WranglerReadinessOptions {
35
+ /** Max time to wait for wrangler to report ready before retrying. */
36
+ startupTimeout?: Duration.DurationInput
37
+ /** Max time for the HTTP connectivity check after wrangler reports ready. */
38
+ connectTimeout?: Duration.DurationInput
39
+ /** Retry policy for startup attempts (applies when startupTimeout elapses or wrangler throws). */
40
+ retrySchedule?: Schedule.Schedule<unknown, unknown, never>
41
+ }
42
+
43
+ export interface StartWranglerDevServerArgs {
44
+ /** Path to wrangler.toml (defaults to cwd/wrangler.toml). */
45
+ wranglerConfigPath?: string
46
+ /** Working directory wrangler should use. */
47
+ cwd: string
48
+ /** The port to try first. The dev server may bind a different port if unavailable. */
49
+ preferredPort?: number
50
+ /** @default false */
51
+ showLogs?: boolean
52
+ /** Optional inspector port for wrangler dev. */
53
+ inspectorPort?: number
54
+ /** Readiness and retry configuration for bringing up wrangler and confirming connectivity. */
55
+ readiness?: WranglerReadinessOptions
56
+ }
57
+
58
+ /**
59
+ * WranglerDevServer as an Effect.Service.
60
+ *
61
+ * This service provides the WranglerDevServer properties and can be accessed
62
+ * directly to get port and url.
63
+ *
64
+ * TODO: Allow for config to be passed in via code instead of `wrangler.toml` file
65
+ * (would need to be placed in temporary file as wrangler only accepts files as config)
66
+ */
67
+ export class WranglerDevServerService extends Effect.Service<WranglerDevServerService>()('WranglerDevServerService', {
68
+ scoped: (args: StartWranglerDevServerArgs) =>
69
+ Effect.gen(function* () {
70
+ const showLogs = args.showLogs ?? false
71
+
72
+ // Allocate preferred port (Wrangler may bind a different one if unavailable)
73
+ const preferredPort =
74
+ args.preferredPort ??
75
+ (yield* getFreePort.pipe(
76
+ Effect.mapError(
77
+ (cause) => new WranglerDevServerError({ cause, message: 'Failed to get free port', port: -1 }),
78
+ ),
79
+ ))
80
+
81
+ yield* Effect.annotateCurrentSpan({ preferredPort })
82
+
83
+ const configPath = path.resolve(args.wranglerConfigPath ?? path.join(args.cwd, 'wrangler.toml'))
84
+
85
+ const fs = yield* FileSystem.FileSystem
86
+ const configContent = yield* fs.readFileString(configPath)
87
+ const parsedConfig = yield* Effect.try(() => Toml.parse(configContent)).pipe(
88
+ Effect.andThen(Schema.decodeUnknown(Schema.Struct({ main: Schema.String }))),
89
+ Effect.mapError(
90
+ (error) => new WranglerDevServerError({ cause: error, message: 'Failed to parse wrangler config', port: -1 }),
91
+ ),
92
+ )
93
+ const resolvedMainPath = yield* Effect.try(() => path.resolve(args.cwd, parsedConfig.main))
94
+
95
+ const readiness = args.readiness ?? {}
96
+ const startupTimeout = readiness.startupTimeout ?? Duration.seconds(IS_CI ? 30 : 10)
97
+ const devServer = yield* Effect.promise(() =>
98
+ wrangler.unstable_dev(resolvedMainPath, {
99
+ config: configPath,
100
+ port: preferredPort,
101
+ inspectorPort: args.inspectorPort ?? 0,
102
+ persistTo: path.join(args.cwd, '.wrangler/state'),
103
+ logLevel: showLogs ? 'debug' : 'none',
104
+ experimental: {
105
+ disableExperimentalWarning: true,
106
+ },
107
+ }),
108
+ ).pipe(
109
+ Effect.timeout(startupTimeout),
110
+ Effect.mapError(
111
+ (cause) =>
112
+ new WranglerDevServerError({
113
+ cause,
114
+ message: `Failed to start wrangler dev server within ${Duration.format(startupTimeout)}`,
115
+ port: preferredPort,
116
+ }),
117
+ ),
118
+ Effect.tapError((error) =>
119
+ Effect.logError('Wrangler dev server failed to start', {
120
+ message: error.message,
121
+ preferredPort,
122
+ cwd: args.cwd,
123
+ }),
124
+ ),
125
+ Effect.retry(readiness.retrySchedule ?? Schedule.recurs(1)),
126
+ )
127
+
128
+ yield* Effect.addFinalizer(
129
+ Effect.fn(
130
+ function* (exit) {
131
+ if (exit._tag === 'Failure' && Cause.isInterruptedOnly(exit.cause) === false) {
132
+ yield* Effect.logError('Closing wrangler dev server on failure', exit.cause)
133
+ }
134
+
135
+ yield* Effect.tryPromise(async () => {
136
+ await devServer.stop()
137
+ // TODO investigate whether we need to wait until exit (see workers-sdk repo/talk to Cloudflare team)
138
+ // await devServer.waitUntilExit()
139
+ })
140
+ },
141
+ Effect.timeout('5 seconds'),
142
+ Effect.orDie,
143
+ Effect.tapCauseLogPretty,
144
+ Effect.withSpan('WranglerDevServerService:stopDevServer'),
145
+ ),
146
+ )
147
+
148
+ const actualPort = devServer.port
149
+ const actualHost = devServer.address
150
+ const url = `http://${actualHost}:${actualPort}`
151
+
152
+ // Use longer timeout in CI environments to account for slower HTTP readiness
153
+ const defaultConnectivityTimeout = Duration.seconds(IS_CI ? 30 : 5)
154
+ const connectivityTimeout = readiness.connectTimeout ?? defaultConnectivityTimeout
155
+
156
+ yield* verifyHttpConnectivity({ url, showLogs, connectTimeout: connectivityTimeout })
157
+
158
+ if (showLogs) {
159
+ yield* Effect.logDebug(
160
+ `Wrangler dev server ready and accepting connections on port ${actualPort} (preferred: ${preferredPort})`,
161
+ )
162
+ }
163
+
164
+ return {
165
+ port: actualPort,
166
+ url,
167
+ } satisfies WranglerDevServer
168
+ }).pipe(
169
+ Effect.mapError((error) =>
170
+ error instanceof WranglerDevServerError
171
+ ? error
172
+ : new WranglerDevServerError({ cause: error, message: 'Failed to start wrangler dev server', port: -1 }),
173
+ ),
174
+ Effect.withSpan('WranglerDevServerService', {
175
+ attributes: { preferredPort: args.preferredPort ?? 'auto', cwd: args.cwd },
176
+ }),
177
+ ),
178
+ }) {}
179
+
180
+ /**
181
+ * Verifies the server is actually accepting HTTP connections by making a test request
182
+ */
183
+ const verifyHttpConnectivity = ({
184
+ url,
185
+ showLogs,
186
+ connectTimeout,
187
+ }: {
188
+ url: string
189
+ showLogs: boolean
190
+ connectTimeout: Duration.DurationInput
191
+ }): Effect.Effect<void, WranglerDevServerError, HttpClient.HttpClient> =>
192
+ Effect.gen(function* () {
193
+ const client = yield* HttpClient.HttpClient
194
+
195
+ if (showLogs) {
196
+ yield* Effect.logDebug(`Verifying HTTP connectivity to ${url}`)
197
+ }
198
+
199
+ // Try to connect with retries using exponential backoff
200
+ yield* client.get(url).pipe(
201
+ Effect.retryOrElse(
202
+ Schedule.exponential('50 millis', 2).pipe(
203
+ Schedule.jittered,
204
+ Schedule.intersect(Schedule.elapsed.pipe(Schedule.whileOutput(Duration.lessThanOrEqualTo(connectTimeout)))),
205
+ Schedule.compose(Schedule.count),
206
+ ),
207
+ (error, attemptCount) =>
208
+ Effect.fail(
209
+ new WranglerDevServerError({
210
+ cause: error,
211
+ message: `Failed to establish HTTP connection to Wrangler server at ${url} after ${attemptCount} attempts (timeout: ${Duration.toMillis(connectTimeout)}ms)`,
212
+ port: 0,
213
+ }),
214
+ ),
215
+ ),
216
+ Effect.tap(() => (showLogs ? Effect.logDebug(`HTTP connectivity verified for ${url}`) : Effect.void)),
217
+ Effect.asVoid,
218
+ Effect.withSpan('verifyHttpConnectivity'),
219
+ )
220
+ })
@@ -0,0 +1,11 @@
1
+ export default {
2
+ async fetch(_request: Request): Promise<Response> {
3
+ return new Response('Hello from Wrangler Dev Server test worker!')
4
+ },
5
+ }
6
+
7
+ export class TestDO {
8
+ async fetch(_request: Request): Promise<Response> {
9
+ return new Response('Hello from Test Durable Object!')
10
+ }
11
+ }
@@ -0,0 +1,11 @@
1
+ name = "wrangler-dev-server-test"
2
+ main = "./cf-worker.ts"
3
+ compatibility_date = "2024-05-12"
4
+
5
+ [[durable_objects.bindings]]
6
+ name = "TEST_DO"
7
+ class_name = "TestDO"
8
+
9
+ [[migrations]]
10
+ tag = "v1"
11
+ new_sqlite_classes = ["TestDO"]
@@ -0,0 +1,6 @@
1
+ export {
2
+ type StartWranglerDevServerArgs,
3
+ type WranglerDevServer,
4
+ WranglerDevServerError,
5
+ WranglerDevServerService,
6
+ } from './WranglerDevServer.ts'
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=polyfill.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"polyfill.d.ts","sourceRoot":"","sources":["../../src/node-vitest/polyfill.ts"],"names":[],"mappings":""}
@@ -1,3 +0,0 @@
1
- process.stdout.isTTY = true;
2
- export {};
3
- //# sourceMappingURL=polyfill.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"polyfill.js","sourceRoot":"","sources":["../../src/node-vitest/polyfill.ts"],"names":[],"mappings":"AAAA,OAAO,CAAC,MAAM,CAAC,KAAK,GAAG,IAAI,CAAA"}