@apifuse/provider-sdk 2.1.0-beta.0 → 2.1.0-beta.10

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 (213) hide show
  1. package/AUTHORING.md +218 -21
  2. package/CHANGELOG.md +54 -0
  3. package/README.md +147 -10
  4. package/SUBMISSION.md +87 -0
  5. package/bin/apifuse-check.ts +86 -4
  6. package/bin/apifuse-dev.ts +87 -13
  7. package/bin/apifuse-pack-check.ts +120 -0
  8. package/bin/apifuse-pack-smoke.ts +423 -0
  9. package/bin/apifuse-perf.ts +142 -49
  10. package/bin/apifuse-record.ts +182 -104
  11. package/bin/apifuse-submit-check.ts +2538 -0
  12. package/bin/apifuse.ts +1 -1
  13. package/dist/ceremonies/index.d.ts +41 -0
  14. package/dist/ceremonies/index.js +490 -0
  15. package/dist/choice-token.d.ts +24 -0
  16. package/dist/choice-token.js +74 -0
  17. package/dist/cli/commands.d.ts +10 -0
  18. package/dist/cli/commands.js +80 -0
  19. package/dist/cli/create.d.ts +47 -0
  20. package/dist/cli/create.js +762 -0
  21. package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
  22. package/dist/cli/templates/provider/.gitignore.tpl +22 -0
  23. package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
  24. package/dist/cli/templates/provider/README.md.tpl +160 -0
  25. package/dist/cli/templates/provider/dev.ts.tpl +5 -0
  26. package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
  27. package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
  28. package/dist/cli/templates/provider/index.ts.tpl +15 -0
  29. package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
  30. package/dist/cli/templates/provider/meta.ts.tpl +7 -0
  31. package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
  32. package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
  33. package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  34. package/dist/cli/templates/provider/start.ts.tpl +5 -0
  35. package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
  36. package/dist/config/loader.d.ts +107 -0
  37. package/dist/config/loader.js +935 -0
  38. package/dist/contract-json.d.ts +9 -0
  39. package/dist/contract-json.js +51 -0
  40. package/dist/contract-serialization.d.ts +4 -0
  41. package/dist/contract-serialization.js +78 -0
  42. package/dist/contract-types.d.ts +49 -0
  43. package/dist/contract-types.js +1 -0
  44. package/dist/contract.d.ts +6 -0
  45. package/dist/contract.js +155 -0
  46. package/dist/define.d.ts +97 -0
  47. package/dist/define.js +1320 -0
  48. package/dist/dev.d.ts +9 -0
  49. package/dist/dev.js +15 -0
  50. package/dist/errors.d.ts +59 -0
  51. package/dist/errors.js +97 -0
  52. package/dist/i18n/catalog.d.ts +29 -0
  53. package/dist/i18n/catalog.js +159 -0
  54. package/dist/i18n/index.d.ts +2 -0
  55. package/dist/i18n/index.js +2 -0
  56. package/dist/i18n/keys.d.ts +10 -0
  57. package/dist/i18n/keys.js +34 -0
  58. package/dist/index.d.ts +41 -0
  59. package/dist/index.js +37 -0
  60. package/dist/lint.d.ts +73 -0
  61. package/dist/lint.js +702 -0
  62. package/dist/observability.d.ts +5 -0
  63. package/dist/observability.js +39 -0
  64. package/dist/provider.d.ts +9 -0
  65. package/dist/provider.js +8 -0
  66. package/dist/public-schema-field-lint.d.ts +2 -0
  67. package/dist/public-schema-field-lint.js +158 -0
  68. package/dist/recipes/gov-api.d.ts +19 -0
  69. package/dist/recipes/gov-api.js +72 -0
  70. package/dist/recipes/rest-api.d.ts +21 -0
  71. package/dist/recipes/rest-api.js +115 -0
  72. package/dist/runtime/auth-flow.d.ts +14 -0
  73. package/dist/runtime/auth-flow.js +44 -0
  74. package/dist/runtime/browser.d.ts +25 -0
  75. package/dist/runtime/browser.js +1034 -0
  76. package/dist/runtime/cache.d.ts +10 -0
  77. package/dist/runtime/cache.js +372 -0
  78. package/dist/runtime/choice.d.ts +15 -0
  79. package/dist/runtime/choice.js +435 -0
  80. package/dist/runtime/credential.d.ts +8 -0
  81. package/dist/runtime/credential.js +61 -0
  82. package/dist/runtime/env.d.ts +2 -0
  83. package/dist/runtime/env.js +10 -0
  84. package/dist/runtime/executor.d.ts +16 -0
  85. package/dist/runtime/executor.js +51 -0
  86. package/dist/runtime/http.d.ts +8 -0
  87. package/dist/runtime/http.js +706 -0
  88. package/dist/runtime/insights.d.ts +9 -0
  89. package/dist/runtime/insights.js +324 -0
  90. package/dist/runtime/instrumentation.d.ts +8 -0
  91. package/dist/runtime/instrumentation.js +269 -0
  92. package/dist/runtime/key-derivation.d.ts +24 -0
  93. package/dist/runtime/key-derivation.js +73 -0
  94. package/dist/runtime/keyring.d.ts +25 -0
  95. package/dist/runtime/keyring.js +93 -0
  96. package/dist/runtime/namespace.d.ts +9 -0
  97. package/dist/runtime/namespace.js +19 -0
  98. package/dist/runtime/otlp.d.ts +39 -0
  99. package/dist/runtime/otlp.js +103 -0
  100. package/dist/runtime/perf.d.ts +12 -0
  101. package/dist/runtime/perf.js +52 -0
  102. package/dist/runtime/prevalidate.d.ts +12 -0
  103. package/dist/runtime/prevalidate.js +173 -0
  104. package/dist/runtime/provider.d.ts +2 -0
  105. package/dist/runtime/provider.js +11 -0
  106. package/dist/runtime/proxy-errors.d.ts +21 -0
  107. package/dist/runtime/proxy-errors.js +83 -0
  108. package/dist/runtime/proxy-telemetry.d.ts +8 -0
  109. package/dist/runtime/proxy-telemetry.js +174 -0
  110. package/dist/runtime/redis.d.ts +17 -0
  111. package/dist/runtime/redis.js +82 -0
  112. package/dist/runtime/request-options.d.ts +3 -0
  113. package/dist/runtime/request-options.js +42 -0
  114. package/dist/runtime/state.d.ts +17 -0
  115. package/dist/runtime/state.js +344 -0
  116. package/dist/runtime/stealth.d.ts +18 -0
  117. package/dist/runtime/stealth.js +834 -0
  118. package/dist/runtime/stt.d.ts +22 -0
  119. package/dist/runtime/stt.js +480 -0
  120. package/dist/runtime/trace.d.ts +26 -0
  121. package/dist/runtime/trace.js +142 -0
  122. package/dist/runtime/waterfall.d.ts +12 -0
  123. package/dist/runtime/waterfall.js +147 -0
  124. package/dist/schema.d.ts +74 -0
  125. package/dist/schema.js +243 -0
  126. package/dist/serve.d.ts +1 -0
  127. package/dist/serve.js +1 -0
  128. package/dist/server/index.d.ts +3 -0
  129. package/dist/server/index.js +2 -0
  130. package/dist/server/serve.d.ts +64 -0
  131. package/dist/server/serve.js +1110 -0
  132. package/dist/server/types.d.ts +136 -0
  133. package/dist/server/types.js +86 -0
  134. package/dist/stealth/profiles.d.ts +4 -0
  135. package/dist/stealth/profiles.js +259 -0
  136. package/dist/stream.d.ts +44 -0
  137. package/dist/stream.js +151 -0
  138. package/dist/testing/helpers.d.ts +23 -0
  139. package/dist/testing/helpers.js +95 -0
  140. package/dist/testing/index.d.ts +2 -0
  141. package/dist/testing/index.js +2 -0
  142. package/dist/testing/run.d.ts +34 -0
  143. package/dist/testing/run.js +303 -0
  144. package/dist/types.d.ts +1326 -0
  145. package/dist/types.js +61 -0
  146. package/dist/utils/date.d.ts +6 -0
  147. package/dist/utils/date.js +101 -0
  148. package/dist/utils/parse.d.ts +16 -0
  149. package/dist/utils/parse.js +51 -0
  150. package/dist/utils/text.d.ts +4 -0
  151. package/dist/utils/text.js +14 -0
  152. package/dist/utils/transform.d.ts +8 -0
  153. package/dist/utils/transform.js +48 -0
  154. package/package.json +57 -29
  155. package/src/ceremonies/index.ts +30 -3
  156. package/src/choice-token.ts +165 -0
  157. package/src/cli/commands.ts +34 -11
  158. package/src/cli/create.ts +214 -52
  159. package/src/cli/templates/provider/.dockerignore.tpl +22 -0
  160. package/src/cli/templates/provider/.gitignore.tpl +22 -0
  161. package/src/cli/templates/provider/README.md.tpl +134 -2
  162. package/src/cli/templates/provider/dev.ts.tpl +1 -1
  163. package/src/cli/templates/provider/domain/README.md.tpl +3 -0
  164. package/src/cli/templates/provider/index.ts.tpl +5 -44
  165. package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
  166. package/src/cli/templates/provider/meta.ts.tpl +7 -0
  167. package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
  168. package/src/cli/templates/provider/operations/ping.ts.tpl +24 -0
  169. package/src/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  170. package/src/cli/templates/provider/start.ts.tpl +1 -1
  171. package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
  172. package/src/config/loader.ts +1282 -7
  173. package/src/contract-json.ts +75 -0
  174. package/src/contract-serialization.ts +89 -0
  175. package/src/contract-types.ts +52 -0
  176. package/src/contract.ts +215 -0
  177. package/src/define.ts +1726 -48
  178. package/src/errors.ts +27 -0
  179. package/src/i18n/catalog.ts +277 -0
  180. package/src/i18n/index.ts +2 -0
  181. package/src/i18n/keys.ts +64 -0
  182. package/src/index.ts +174 -15
  183. package/src/lint.ts +547 -73
  184. package/src/observability.ts +41 -0
  185. package/src/provider.ts +104 -5
  186. package/src/public-schema-field-lint.ts +237 -0
  187. package/src/runtime/auth-flow.ts +7 -0
  188. package/src/runtime/browser.ts +762 -51
  189. package/src/runtime/cache.ts +528 -0
  190. package/src/runtime/choice.ts +760 -0
  191. package/src/runtime/executor.ts +32 -3
  192. package/src/runtime/http.ts +945 -185
  193. package/src/runtime/insights.ts +11 -11
  194. package/src/runtime/instrumentation.ts +12 -4
  195. package/src/runtime/key-derivation.ts +1 -1
  196. package/src/runtime/keyring.ts +4 -3
  197. package/src/runtime/proxy-errors.ts +132 -0
  198. package/src/runtime/proxy-telemetry.ts +253 -0
  199. package/src/runtime/redis.ts +116 -0
  200. package/src/runtime/request-options.ts +66 -0
  201. package/src/runtime/state.ts +563 -0
  202. package/src/runtime/stealth.ts +1159 -0
  203. package/src/runtime/stt.ts +629 -0
  204. package/src/runtime/trace.ts +1 -1
  205. package/src/schema.ts +363 -1
  206. package/src/server/serve.ts +1172 -76
  207. package/src/server/types.ts +37 -0
  208. package/src/stream.ts +210 -0
  209. package/src/testing/run.ts +31 -5
  210. package/src/types.ts +1118 -44
  211. package/src/composite.ts +0 -43
  212. package/src/runtime/tls.ts +0 -425
  213. package/src/types/playwright-stealth.d.ts +0 -9
@@ -0,0 +1,423 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import {
4
+ type ChildProcess,
5
+ execFileSync,
6
+ spawn,
7
+ spawnSync,
8
+ } from "node:child_process";
9
+ import {
10
+ existsSync,
11
+ mkdirSync,
12
+ mkdtempSync,
13
+ readFileSync,
14
+ rmSync,
15
+ writeFileSync,
16
+ } from "node:fs";
17
+ import { createServer } from "node:net";
18
+ import { tmpdir } from "node:os";
19
+ import { join, resolve } from "node:path";
20
+ import { z } from "zod";
21
+
22
+ const PACK_RESULT_SCHEMA = z.array(
23
+ z.object({
24
+ filename: z.string(),
25
+ }),
26
+ );
27
+ const HEALTH_RESPONSE_SCHEMA = z.object({
28
+ status: z.string(),
29
+ provider: z.string(),
30
+ version: z.string().optional(),
31
+ });
32
+ const PING_RESPONSE_SCHEMA = z.object({
33
+ data: z
34
+ .object({
35
+ ok: z.boolean(),
36
+ message: z.string(),
37
+ })
38
+ .optional(),
39
+ error: z.unknown().optional(),
40
+ });
41
+
42
+ const KEEP_TEMP = process.env.APIFUSE__PACK_SMOKE__KEEP_TEMP === "1";
43
+
44
+ const tempRoot = mkdtempSync(
45
+ join(tmpdir(), "apifuse-provider-sdk-pack-smoke-"),
46
+ );
47
+ const packDir = join(tempRoot, "pack");
48
+ const consumerDir = join(tempRoot, "consumer");
49
+ const externalWorkspaceDir = join(tempRoot, "external-workspace");
50
+
51
+ try {
52
+ mkdirSync(packDir, { recursive: true });
53
+ mkdirSync(consumerDir, { recursive: true });
54
+ mkdirSync(join(externalWorkspaceDir, "providers"), { recursive: true });
55
+
56
+ const packed = packSdk(packDir);
57
+ const tarballPath = resolve(packDir, packed.filename);
58
+ const tarballSpecifier = `file:${tarballPath}`;
59
+
60
+ writeFileSync(
61
+ join(consumerDir, "package.json"),
62
+ `${JSON.stringify(
63
+ {
64
+ private: true,
65
+ type: "module",
66
+ dependencies: {
67
+ "@apifuse/provider-sdk": tarballSpecifier,
68
+ },
69
+ },
70
+ null,
71
+ 2,
72
+ )}\n`,
73
+ );
74
+
75
+ run("bun", ["install"], consumerDir);
76
+
77
+ const cliBin = join(consumerDir, "node_modules", ".bin", "apifuse");
78
+ if (!existsSync(cliBin)) {
79
+ throw new Error(`Expected CLI bin at ${cliBin}`);
80
+ }
81
+
82
+ run(
83
+ "bun",
84
+ [
85
+ cliBin,
86
+ "create",
87
+ "dx-smoke",
88
+ "--yes",
89
+ "--json",
90
+ "--sdk-specifier",
91
+ tarballSpecifier,
92
+ ],
93
+ consumerDir,
94
+ );
95
+
96
+ const generatedProviderDir = join(consumerDir, "dx-smoke");
97
+ run("bun", ["run", "check"], generatedProviderDir);
98
+ run("bun", ["run", "submit-check"], generatedProviderDir);
99
+ run("bun", ["run", "test"], generatedProviderDir);
100
+ assertGeneratedReadme(generatedProviderDir);
101
+ await smokeGeneratedDevServer(generatedProviderDir);
102
+ assertExternalWorkspaceTopology(
103
+ cliBin,
104
+ externalWorkspaceDir,
105
+ tarballSpecifier,
106
+ );
107
+
108
+ console.log(
109
+ `Provider SDK packed-artifact smoke passed: ${tarballPath} -> ${generatedProviderDir}`,
110
+ );
111
+ } finally {
112
+ if (KEEP_TEMP) {
113
+ console.log(`Keeping smoke temp directory: ${tempRoot}`);
114
+ } else {
115
+ rmSync(tempRoot, { recursive: true, force: true });
116
+ }
117
+ }
118
+
119
+ function assertExternalWorkspaceTopology(
120
+ cliBin: string,
121
+ externalWorkspaceDir: string,
122
+ tarballSpecifier: string,
123
+ ): void {
124
+ writeFileSync(
125
+ join(externalWorkspaceDir, "package.json"),
126
+ `${JSON.stringify(
127
+ {
128
+ private: true,
129
+ type: "module",
130
+ workspaces: ["providers/*"],
131
+ },
132
+ null,
133
+ 2,
134
+ )}\n`,
135
+ );
136
+
137
+ run(
138
+ "bun",
139
+ [
140
+ cliBin,
141
+ "create",
142
+ "external-workspace-smoke",
143
+ "--yes",
144
+ "--json",
145
+ "--sdk-specifier",
146
+ tarballSpecifier,
147
+ ],
148
+ externalWorkspaceDir,
149
+ );
150
+
151
+ const generatedProviderDir = join(
152
+ externalWorkspaceDir,
153
+ "external-workspace-smoke",
154
+ );
155
+ const forbiddenProviderDir = join(
156
+ externalWorkspaceDir,
157
+ "providers",
158
+ "external-workspace-smoke",
159
+ );
160
+ if (!existsSync(generatedProviderDir)) {
161
+ throw new Error(
162
+ "Public create must generate a one-provider repository at <name>/ even when providers/ exists.",
163
+ );
164
+ }
165
+ if (existsSync(forbiddenProviderDir)) {
166
+ throw new Error(
167
+ "Public create must not generate providers/<name>/ in external bounty workspaces.",
168
+ );
169
+ }
170
+
171
+ const packageJson = JSON.parse(
172
+ readFileSync(join(generatedProviderDir, "package.json"), "utf8"),
173
+ );
174
+ const sdkDependency = packageJson?.dependencies?.["@apifuse/provider-sdk"];
175
+ if (sdkDependency !== tarballSpecifier) {
176
+ throw new Error(
177
+ `Expected generated provider to depend on packed SDK ${tarballSpecifier}, got ${sdkDependency}`,
178
+ );
179
+ }
180
+ if (JSON.stringify(packageJson).includes("workspace:")) {
181
+ throw new Error(
182
+ "External bounty workspace scaffold must not contain workspace: dependencies.",
183
+ );
184
+ }
185
+
186
+ run("bun", ["install"], generatedProviderDir);
187
+ run("bun", ["run", "check"], generatedProviderDir);
188
+ run("bun", ["run", "submit-check"], generatedProviderDir);
189
+ run("bun", ["run", "test"], generatedProviderDir);
190
+
191
+ const monorepoAttempt = spawnSync(
192
+ "bun",
193
+ [cliBin, "create", "bad-monorepo-smoke", "--preset", "monorepo", "--yes"],
194
+ {
195
+ cwd: externalWorkspaceDir,
196
+ env: { ...process.env, APIFUSE__SDK__SPECIFIER: tarballSpecifier },
197
+ encoding: "utf8",
198
+ stdio: ["ignore", "pipe", "pipe"],
199
+ },
200
+ );
201
+ if (monorepoAttempt.status === 0) {
202
+ throw new Error(
203
+ "--preset monorepo must reject outside the private APIFuse monorepo.",
204
+ );
205
+ }
206
+ const rejectionOutput = `${monorepoAttempt.stdout}\n${monorepoAttempt.stderr}`;
207
+ if (
208
+ !rejectionOutput.includes(
209
+ "Monorepo preset is internal to the APIFuse repository",
210
+ )
211
+ ) {
212
+ throw new Error(`Unexpected monorepo rejection output: ${rejectionOutput}`);
213
+ }
214
+ }
215
+
216
+ function packSdk(destination: string): { filename: string } {
217
+ const raw = execFileSync(
218
+ "npm",
219
+ ["pack", "--json", "--pack-destination", destination],
220
+ {
221
+ cwd: process.cwd(),
222
+ encoding: "utf8",
223
+ stdio: ["ignore", "pipe", "inherit"],
224
+ },
225
+ );
226
+ const parsed = PACK_RESULT_SCHEMA.parse(JSON.parse(raw));
227
+ const first = parsed[0];
228
+ if (!first) {
229
+ throw new Error("npm pack --json returned no package metadata.");
230
+ }
231
+ return first;
232
+ }
233
+
234
+ function run(command: string, args: string[], cwd: string): void {
235
+ const result = spawnSync(command, args, {
236
+ cwd,
237
+ env: process.env,
238
+ stdio: "inherit",
239
+ });
240
+
241
+ if (result.error) {
242
+ throw result.error;
243
+ }
244
+
245
+ if (result.status !== 0) {
246
+ throw new Error(
247
+ `Command failed (${[command, ...args].join(" ")}) in ${cwd} with exit code ${result.status}`,
248
+ );
249
+ }
250
+ }
251
+
252
+ function assertGeneratedReadme(providerDir: string): void {
253
+ const readme = readFileSync(join(providerDir, "README.md"), "utf8");
254
+ if (!readme.includes('"requestId":"req_local_ping"')) {
255
+ throw new Error(
256
+ "Generated README is missing requestId in local smoke docs.",
257
+ );
258
+ }
259
+ if (readme.includes('"connection":null')) {
260
+ throw new Error(
261
+ "Generated README must not document connection:null for no-auth local smoke.",
262
+ );
263
+ }
264
+ if (!readme.includes("bunx playwright install chromium")) {
265
+ throw new Error(
266
+ "Generated README is missing browser runtime troubleshooting guidance.",
267
+ );
268
+ }
269
+ if (!readme.includes("impit")) {
270
+ throw new Error(
271
+ "Generated README is missing impit stealth runtime guidance.",
272
+ );
273
+ }
274
+ if (!readme.includes("bun run submit-check")) {
275
+ throw new Error(
276
+ "Generated README must document the submit-check pre-submission workflow.",
277
+ );
278
+ }
279
+ if (!readme.includes("bun run record -- --operation <operation>")) {
280
+ throw new Error(
281
+ "Generated README must document fixture recording through the generated record script.",
282
+ );
283
+ }
284
+ }
285
+
286
+ async function smokeGeneratedDevServer(providerDir: string): Promise<void> {
287
+ const port = await getAvailablePort();
288
+ const server = spawn("bun", ["run", "dev"], {
289
+ cwd: providerDir,
290
+ env: { ...process.env, APIFUSE__RUNTIME__PORT: String(port) },
291
+ detached: process.platform !== "win32",
292
+ stdio: ["ignore", "pipe", "pipe"],
293
+ });
294
+ let output = "";
295
+ server.stdout?.on("data", (chunk) => {
296
+ output += chunk.toString();
297
+ });
298
+ server.stderr?.on("data", (chunk) => {
299
+ output += chunk.toString();
300
+ });
301
+
302
+ try {
303
+ const baseUrl = `http://127.0.0.1:${port}`;
304
+ await waitForHttp(`${baseUrl}/health`, server, () => output);
305
+
306
+ const health = await fetchJson(`${baseUrl}/health`, HEALTH_RESPONSE_SCHEMA);
307
+ if (health.status !== "ok" || health.provider !== "dx-smoke") {
308
+ throw new Error(`Unexpected /health payload: ${JSON.stringify(health)}`);
309
+ }
310
+
311
+ const response = await fetch(`${baseUrl}/v1/ping`, {
312
+ method: "POST",
313
+ headers: { "content-type": "application/json" },
314
+ body: JSON.stringify({
315
+ requestId: "req_pack_smoke_ping",
316
+ input: { value: "hello" },
317
+ headers: {},
318
+ }),
319
+ });
320
+ const payload = PING_RESPONSE_SCHEMA.parse(await response.json());
321
+
322
+ if (!response.ok || payload.data?.ok !== true) {
323
+ throw new Error(
324
+ `Unexpected /v1/ping response (${response.status}): ${JSON.stringify(payload)}`,
325
+ );
326
+ }
327
+ } finally {
328
+ await stopServer(server);
329
+ }
330
+ }
331
+
332
+ async function getAvailablePort(): Promise<number> {
333
+ return await new Promise((resolvePromise, rejectPromise) => {
334
+ const server = createServer();
335
+ server.once("error", rejectPromise);
336
+ server.listen(0, "127.0.0.1", () => {
337
+ const address = server.address();
338
+ server.close((error) => {
339
+ if (error) {
340
+ rejectPromise(error);
341
+ return;
342
+ }
343
+ if (!address || typeof address === "string") {
344
+ rejectPromise(new Error("Could not allocate a local TCP port."));
345
+ return;
346
+ }
347
+ resolvePromise(address.port);
348
+ });
349
+ });
350
+ });
351
+ }
352
+
353
+ async function fetchJson<T>(url: string, schema: z.ZodType<T>): Promise<T> {
354
+ const response = await fetch(url);
355
+ if (!response.ok) {
356
+ throw new Error(`${url} returned ${response.status}`);
357
+ }
358
+ return schema.parse(await response.json());
359
+ }
360
+
361
+ async function waitForHttp(
362
+ url: string,
363
+ server: ChildProcess,
364
+ getOutput: () => string,
365
+ ): Promise<void> {
366
+ const deadline = Date.now() + 10_000;
367
+ let lastError: unknown;
368
+
369
+ while (Date.now() < deadline) {
370
+ if (server.exitCode !== null) {
371
+ throw new Error(
372
+ `Dev server exited early with code ${server.exitCode}\n${getOutput()}`,
373
+ );
374
+ }
375
+
376
+ try {
377
+ await fetchJson(url, HEALTH_RESPONSE_SCHEMA);
378
+ return;
379
+ } catch (error) {
380
+ lastError = error;
381
+ await new Promise((resolve) => setTimeout(resolve, 200));
382
+ }
383
+ }
384
+
385
+ throw new Error(
386
+ `Timed out waiting for ${url}: ${lastError instanceof Error ? lastError.message : String(lastError)}\n${getOutput()}`,
387
+ );
388
+ }
389
+
390
+ async function stopServer(server: ChildProcess): Promise<void> {
391
+ if (server.exitCode !== null) {
392
+ return;
393
+ }
394
+ killProcessTree(server, "SIGTERM");
395
+ await new Promise<void>((resolvePromise) => {
396
+ const timeout = setTimeout(() => {
397
+ if (server.exitCode === null) {
398
+ killProcessTree(server, "SIGKILL");
399
+ }
400
+ resolvePromise();
401
+ }, 2_000);
402
+ server.once("exit", () => {
403
+ clearTimeout(timeout);
404
+ resolvePromise();
405
+ });
406
+ });
407
+ }
408
+
409
+ function killProcessTree(server: ChildProcess, signal: NodeJS.Signals): void {
410
+ if (server.pid === undefined) {
411
+ return;
412
+ }
413
+
414
+ try {
415
+ if (process.platform === "win32") {
416
+ server.kill(signal);
417
+ return;
418
+ }
419
+ process.kill(-server.pid, signal);
420
+ } catch {
421
+ server.kill(signal);
422
+ }
423
+ }