@chidchanun/bcp 0.1.10 → 0.1.12
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/CHANGELOG.md +43 -2
- package/README.md +131 -9
- package/docs/form-actions.md +314 -0
- package/package.json +1 -1
- package/packages/bundler/src/server-production-actions.ts +523 -0
- package/packages/bundler/src/server-production-middleware.ts +17 -0
- package/packages/client/src/form.tsx +548 -0
- package/packages/client/src/index.tsx +10 -0
- package/packages/client/src/server.ts +6 -0
- package/packages/server/src/form-action-dev-proxy.ts +601 -0
- package/packages/server/src/form-action-transport.ts +271 -0
- package/packages/server/src/form-action.ts +493 -0
- package/packages/server/src/middleware-dev-server.ts +56 -2
- package/packages/server/src/standalone-production-runtime-v2-action.ts +765 -0
- package/packages/server/src/standalone-production-runtime-v3.ts +1 -1
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import * as http from "node:http";
|
|
3
|
+
import * as net from "node:net";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
pathToFileURL,
|
|
7
|
+
} from "node:url";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
createFormActionErrorResponse,
|
|
11
|
+
createFormActionTransportResponse,
|
|
12
|
+
isEnhancedFormActionRequest,
|
|
13
|
+
type CompletedPageAction,
|
|
14
|
+
} from "./form-action-transport.js";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
sendNavigationWebResponse,
|
|
18
|
+
} from "./navigation-response.js";
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
createStandaloneProductionServer as createBaseStandaloneProductionServer,
|
|
22
|
+
type StandaloneApiRoute,
|
|
23
|
+
type StandalonePageRoute,
|
|
24
|
+
type StandaloneProductionServerOptions as BaseStandaloneProductionServerOptions,
|
|
25
|
+
} from "./standalone-production-runtime-v2-guard.js";
|
|
26
|
+
|
|
27
|
+
export type {
|
|
28
|
+
StandaloneApiRoute,
|
|
29
|
+
StandalonePageRoute,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export interface StandaloneProductionServerOptions
|
|
33
|
+
extends BaseStandaloneProductionServerOptions {}
|
|
34
|
+
|
|
35
|
+
interface ProductionActionModule {
|
|
36
|
+
actionPathnames: string[];
|
|
37
|
+
evaluateRouteAction(
|
|
38
|
+
input: {
|
|
39
|
+
url: string;
|
|
40
|
+
name: string;
|
|
41
|
+
method: string;
|
|
42
|
+
formData: FormData;
|
|
43
|
+
headers: Array<[
|
|
44
|
+
string,
|
|
45
|
+
string
|
|
46
|
+
]>;
|
|
47
|
+
remoteAddress: string | null;
|
|
48
|
+
}
|
|
49
|
+
): Promise<CompletedPageAction | null>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createStandaloneProductionServer(
|
|
53
|
+
options: StandaloneProductionServerOptions
|
|
54
|
+
) {
|
|
55
|
+
const actionFile =
|
|
56
|
+
path.join(
|
|
57
|
+
options.buildDirectory,
|
|
58
|
+
"server",
|
|
59
|
+
"actions.mjs"
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (!fs.existsSync(actionFile)) {
|
|
63
|
+
return createBaseStandaloneProductionServer(
|
|
64
|
+
options
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let baseServer:
|
|
69
|
+
ReturnType<
|
|
70
|
+
typeof createBaseStandaloneProductionServer
|
|
71
|
+
> | null =
|
|
72
|
+
null;
|
|
73
|
+
let gateway:
|
|
74
|
+
http.Server | null =
|
|
75
|
+
null;
|
|
76
|
+
let actionModule:
|
|
77
|
+
ProductionActionModule | null =
|
|
78
|
+
null;
|
|
79
|
+
let started =
|
|
80
|
+
false;
|
|
81
|
+
|
|
82
|
+
async function start(): Promise<void> {
|
|
83
|
+
if (started) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
actionModule =
|
|
88
|
+
await import(
|
|
89
|
+
pathToFileURL(
|
|
90
|
+
actionFile
|
|
91
|
+
).href
|
|
92
|
+
) as ProductionActionModule;
|
|
93
|
+
|
|
94
|
+
assertActionModule(
|
|
95
|
+
actionModule,
|
|
96
|
+
actionFile
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const internalPort =
|
|
100
|
+
await findFreePort();
|
|
101
|
+
|
|
102
|
+
baseServer =
|
|
103
|
+
createBaseStandaloneProductionServer({
|
|
104
|
+
...options,
|
|
105
|
+
port:
|
|
106
|
+
internalPort,
|
|
107
|
+
hostname:
|
|
108
|
+
"127.0.0.1",
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await baseServer.start();
|
|
112
|
+
|
|
113
|
+
gateway =
|
|
114
|
+
http.createServer(
|
|
115
|
+
async (
|
|
116
|
+
req,
|
|
117
|
+
res
|
|
118
|
+
) => {
|
|
119
|
+
try {
|
|
120
|
+
const requestUrl =
|
|
121
|
+
new URL(
|
|
122
|
+
req.url ?? "/",
|
|
123
|
+
`http://${req.headers.host ?? `${options.hostname}:${options.port}`}`
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
if (
|
|
127
|
+
requestUrl.pathname !==
|
|
128
|
+
"/_bcp/action"
|
|
129
|
+
) {
|
|
130
|
+
proxyRequest(
|
|
131
|
+
req,
|
|
132
|
+
res,
|
|
133
|
+
internalPort
|
|
134
|
+
);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await handleActionRequest(
|
|
139
|
+
req,
|
|
140
|
+
res,
|
|
141
|
+
requestUrl,
|
|
142
|
+
actionModule
|
|
143
|
+
);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error(
|
|
146
|
+
"[BCP Action Error]",
|
|
147
|
+
error
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
if (
|
|
151
|
+
res.headersSent
|
|
152
|
+
) {
|
|
153
|
+
res.destroy();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
await sendNavigationWebResponse(
|
|
158
|
+
req,
|
|
159
|
+
res,
|
|
160
|
+
createFormActionErrorResponse(
|
|
161
|
+
"Internal Server Error",
|
|
162
|
+
500
|
|
163
|
+
)
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
gateway.keepAliveTimeout =
|
|
170
|
+
65_000;
|
|
171
|
+
gateway.headersTimeout =
|
|
172
|
+
66_000;
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
await listen(
|
|
176
|
+
gateway,
|
|
177
|
+
options.port,
|
|
178
|
+
options.hostname
|
|
179
|
+
);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
await baseServer.stop();
|
|
182
|
+
baseServer =
|
|
183
|
+
null;
|
|
184
|
+
gateway =
|
|
185
|
+
null;
|
|
186
|
+
actionModule =
|
|
187
|
+
null;
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
started =
|
|
192
|
+
true;
|
|
193
|
+
|
|
194
|
+
console.log(
|
|
195
|
+
`[BCP Actions] Production routes: ${actionModule.actionPathnames.length}`
|
|
196
|
+
);
|
|
197
|
+
console.log(
|
|
198
|
+
`[BCP Actions] Gateway: http://${options.hostname}:${options.port}`
|
|
199
|
+
);
|
|
200
|
+
console.log("");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function stop(): Promise<void> {
|
|
204
|
+
if (gateway) {
|
|
205
|
+
await closeServer(
|
|
206
|
+
gateway
|
|
207
|
+
);
|
|
208
|
+
gateway =
|
|
209
|
+
null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (baseServer) {
|
|
213
|
+
await baseServer.stop();
|
|
214
|
+
baseServer =
|
|
215
|
+
null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
actionModule =
|
|
219
|
+
null;
|
|
220
|
+
started =
|
|
221
|
+
false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
start,
|
|
226
|
+
stop,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function handleActionRequest(
|
|
231
|
+
req: http.IncomingMessage,
|
|
232
|
+
res: http.ServerResponse,
|
|
233
|
+
requestUrl: URL,
|
|
234
|
+
actionModule: ProductionActionModule | null
|
|
235
|
+
): Promise<void> {
|
|
236
|
+
if (
|
|
237
|
+
!actionModule
|
|
238
|
+
) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
"BCP Framework: production action module was not loaded."
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (
|
|
245
|
+
(req.method ?? "GET")
|
|
246
|
+
.toUpperCase() !==
|
|
247
|
+
"POST"
|
|
248
|
+
) {
|
|
249
|
+
await sendNavigationWebResponse(
|
|
250
|
+
req,
|
|
251
|
+
res,
|
|
252
|
+
createFormActionErrorResponse(
|
|
253
|
+
"Method Not Allowed",
|
|
254
|
+
405
|
|
255
|
+
)
|
|
256
|
+
);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const actionName =
|
|
261
|
+
requestUrl.searchParams.get(
|
|
262
|
+
"name"
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
if (!actionName) {
|
|
266
|
+
await sendNavigationWebResponse(
|
|
267
|
+
req,
|
|
268
|
+
res,
|
|
269
|
+
createFormActionErrorResponse(
|
|
270
|
+
"Missing form action name.",
|
|
271
|
+
400
|
|
272
|
+
)
|
|
273
|
+
);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const target =
|
|
278
|
+
resolveActionTarget(
|
|
279
|
+
req,
|
|
280
|
+
requestUrl
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
if (!target) {
|
|
284
|
+
await sendNavigationWebResponse(
|
|
285
|
+
req,
|
|
286
|
+
res,
|
|
287
|
+
createFormActionErrorResponse(
|
|
288
|
+
"Missing or invalid form action target.",
|
|
289
|
+
400
|
|
290
|
+
)
|
|
291
|
+
);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const formData =
|
|
296
|
+
await readFormData(
|
|
297
|
+
req,
|
|
298
|
+
requestUrl
|
|
299
|
+
);
|
|
300
|
+
const result =
|
|
301
|
+
await actionModule
|
|
302
|
+
.evaluateRouteAction({
|
|
303
|
+
url:
|
|
304
|
+
target.href,
|
|
305
|
+
name:
|
|
306
|
+
actionName,
|
|
307
|
+
method:
|
|
308
|
+
requestUrl.searchParams.get(
|
|
309
|
+
"method"
|
|
310
|
+
) ??
|
|
311
|
+
"POST",
|
|
312
|
+
formData,
|
|
313
|
+
headers:
|
|
314
|
+
collectRequestHeaders(
|
|
315
|
+
req
|
|
316
|
+
),
|
|
317
|
+
remoteAddress:
|
|
318
|
+
req.socket
|
|
319
|
+
.remoteAddress ??
|
|
320
|
+
null,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
if (!result) {
|
|
324
|
+
await sendNavigationWebResponse(
|
|
325
|
+
req,
|
|
326
|
+
res,
|
|
327
|
+
createFormActionErrorResponse(
|
|
328
|
+
"Form action not found.",
|
|
329
|
+
404
|
|
330
|
+
)
|
|
331
|
+
);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const response =
|
|
336
|
+
createFormActionTransportResponse(
|
|
337
|
+
result,
|
|
338
|
+
target,
|
|
339
|
+
isEnhancedFormActionRequest(
|
|
340
|
+
collectHeaders(
|
|
341
|
+
req
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
await sendNavigationWebResponse(
|
|
347
|
+
req,
|
|
348
|
+
res,
|
|
349
|
+
response
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function assertActionModule(
|
|
354
|
+
value: ProductionActionModule,
|
|
355
|
+
filePath: string
|
|
356
|
+
): void {
|
|
357
|
+
if (
|
|
358
|
+
!Array.isArray(
|
|
359
|
+
value.actionPathnames
|
|
360
|
+
) ||
|
|
361
|
+
typeof value.evaluateRouteAction !==
|
|
362
|
+
"function"
|
|
363
|
+
) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`BCP Framework: invalid standalone action module at "${filePath}".`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function resolveActionTarget(
|
|
371
|
+
req: http.IncomingMessage,
|
|
372
|
+
requestUrl: URL
|
|
373
|
+
): URL | null {
|
|
374
|
+
const candidates = [
|
|
375
|
+
requestUrl.searchParams.get(
|
|
376
|
+
"url"
|
|
377
|
+
),
|
|
378
|
+
headerValue(
|
|
379
|
+
req.headers[
|
|
380
|
+
"x-bcp-action-target"
|
|
381
|
+
]
|
|
382
|
+
),
|
|
383
|
+
headerValue(
|
|
384
|
+
req.headers.referer
|
|
385
|
+
),
|
|
386
|
+
];
|
|
387
|
+
|
|
388
|
+
for (
|
|
389
|
+
const candidate
|
|
390
|
+
of candidates
|
|
391
|
+
) {
|
|
392
|
+
if (!candidate) {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
const target =
|
|
398
|
+
new URL(
|
|
399
|
+
candidate,
|
|
400
|
+
requestUrl.origin
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
if (
|
|
404
|
+
target.origin ===
|
|
405
|
+
requestUrl.origin &&
|
|
406
|
+
target.pathname !==
|
|
407
|
+
"/_bcp/action"
|
|
408
|
+
) {
|
|
409
|
+
return target;
|
|
410
|
+
}
|
|
411
|
+
} catch {
|
|
412
|
+
// Try the next target source.
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function readFormData(
|
|
420
|
+
req: http.IncomingMessage,
|
|
421
|
+
requestUrl: URL
|
|
422
|
+
): Promise<FormData> {
|
|
423
|
+
const chunks:
|
|
424
|
+
Buffer[] = [];
|
|
425
|
+
|
|
426
|
+
for await (
|
|
427
|
+
const chunk
|
|
428
|
+
of req
|
|
429
|
+
) {
|
|
430
|
+
chunks.push(
|
|
431
|
+
Buffer.isBuffer(
|
|
432
|
+
chunk
|
|
433
|
+
)
|
|
434
|
+
? chunk
|
|
435
|
+
: Buffer.from(
|
|
436
|
+
chunk
|
|
437
|
+
)
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const body =
|
|
442
|
+
Buffer.concat(
|
|
443
|
+
chunks
|
|
444
|
+
);
|
|
445
|
+
const request =
|
|
446
|
+
new Request(
|
|
447
|
+
requestUrl,
|
|
448
|
+
{
|
|
449
|
+
method: "POST",
|
|
450
|
+
headers:
|
|
451
|
+
collectHeaders(
|
|
452
|
+
req
|
|
453
|
+
),
|
|
454
|
+
body:
|
|
455
|
+
body.length > 0
|
|
456
|
+
? body
|
|
457
|
+
: undefined,
|
|
458
|
+
}
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
return request.formData();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function collectRequestHeaders(
|
|
465
|
+
req: http.IncomingMessage
|
|
466
|
+
): Array<[
|
|
467
|
+
string,
|
|
468
|
+
string
|
|
469
|
+
]> {
|
|
470
|
+
const result:
|
|
471
|
+
Array<[
|
|
472
|
+
string,
|
|
473
|
+
string
|
|
474
|
+
]> = [];
|
|
475
|
+
|
|
476
|
+
for (
|
|
477
|
+
const [
|
|
478
|
+
name,
|
|
479
|
+
value,
|
|
480
|
+
]
|
|
481
|
+
of Object.entries(
|
|
482
|
+
req.headers
|
|
483
|
+
)
|
|
484
|
+
) {
|
|
485
|
+
if (
|
|
486
|
+
value === undefined
|
|
487
|
+
) {
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (
|
|
492
|
+
Array.isArray(
|
|
493
|
+
value
|
|
494
|
+
)
|
|
495
|
+
) {
|
|
496
|
+
for (
|
|
497
|
+
const item
|
|
498
|
+
of value
|
|
499
|
+
) {
|
|
500
|
+
result.push([
|
|
501
|
+
name,
|
|
502
|
+
item,
|
|
503
|
+
]);
|
|
504
|
+
}
|
|
505
|
+
} else {
|
|
506
|
+
result.push([
|
|
507
|
+
name,
|
|
508
|
+
value,
|
|
509
|
+
]);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return result;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function collectHeaders(
|
|
517
|
+
req: http.IncomingMessage
|
|
518
|
+
): Headers {
|
|
519
|
+
return new Headers(
|
|
520
|
+
collectRequestHeaders(
|
|
521
|
+
req
|
|
522
|
+
)
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function headerValue(
|
|
527
|
+
value: string | string[] | undefined
|
|
528
|
+
): string | null {
|
|
529
|
+
if (
|
|
530
|
+
Array.isArray(
|
|
531
|
+
value
|
|
532
|
+
)
|
|
533
|
+
) {
|
|
534
|
+
return value[0] ??
|
|
535
|
+
null;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return value ??
|
|
539
|
+
null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function proxyRequest(
|
|
543
|
+
req: http.IncomingMessage,
|
|
544
|
+
res: http.ServerResponse,
|
|
545
|
+
upstreamPort: number
|
|
546
|
+
): void {
|
|
547
|
+
const upstream =
|
|
548
|
+
http.request({
|
|
549
|
+
hostname:
|
|
550
|
+
"127.0.0.1",
|
|
551
|
+
port:
|
|
552
|
+
upstreamPort,
|
|
553
|
+
method:
|
|
554
|
+
req.method ??
|
|
555
|
+
"GET",
|
|
556
|
+
path:
|
|
557
|
+
req.url ??
|
|
558
|
+
"/",
|
|
559
|
+
headers:
|
|
560
|
+
req.headers,
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
upstream.on(
|
|
564
|
+
"response",
|
|
565
|
+
(upstreamResponse) => {
|
|
566
|
+
res.writeHead(
|
|
567
|
+
upstreamResponse.statusCode ??
|
|
568
|
+
502,
|
|
569
|
+
upstreamResponse.headers
|
|
570
|
+
);
|
|
571
|
+
upstreamResponse.pipe(
|
|
572
|
+
res
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
upstream.on(
|
|
578
|
+
"error",
|
|
579
|
+
(error) => {
|
|
580
|
+
if (
|
|
581
|
+
res.headersSent
|
|
582
|
+
) {
|
|
583
|
+
res.destroy(
|
|
584
|
+
error
|
|
585
|
+
);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
res.writeHead(
|
|
590
|
+
502,
|
|
591
|
+
{
|
|
592
|
+
"Content-Type":
|
|
593
|
+
"text/plain; charset=utf-8",
|
|
594
|
+
"Cache-Control":
|
|
595
|
+
"no-store",
|
|
596
|
+
}
|
|
597
|
+
);
|
|
598
|
+
res.end(
|
|
599
|
+
"Bad Gateway"
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
req.pipe(
|
|
605
|
+
upstream
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function listen(
|
|
610
|
+
server: http.Server,
|
|
611
|
+
port: number,
|
|
612
|
+
hostname: string
|
|
613
|
+
): Promise<void> {
|
|
614
|
+
return new Promise(
|
|
615
|
+
(
|
|
616
|
+
resolve,
|
|
617
|
+
reject
|
|
618
|
+
) => {
|
|
619
|
+
const onError =
|
|
620
|
+
(error: Error) => {
|
|
621
|
+
server.off(
|
|
622
|
+
"listening",
|
|
623
|
+
onListening
|
|
624
|
+
);
|
|
625
|
+
reject(
|
|
626
|
+
error
|
|
627
|
+
);
|
|
628
|
+
};
|
|
629
|
+
const onListening =
|
|
630
|
+
() => {
|
|
631
|
+
server.off(
|
|
632
|
+
"error",
|
|
633
|
+
onError
|
|
634
|
+
);
|
|
635
|
+
resolve();
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
server.once(
|
|
639
|
+
"error",
|
|
640
|
+
onError
|
|
641
|
+
);
|
|
642
|
+
server.once(
|
|
643
|
+
"listening",
|
|
644
|
+
onListening
|
|
645
|
+
);
|
|
646
|
+
server.listen(
|
|
647
|
+
port,
|
|
648
|
+
hostname
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function closeServer(
|
|
655
|
+
server: http.Server
|
|
656
|
+
): Promise<void> {
|
|
657
|
+
if (
|
|
658
|
+
!server.listening
|
|
659
|
+
) {
|
|
660
|
+
return Promise.resolve();
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return new Promise(
|
|
664
|
+
(
|
|
665
|
+
resolve,
|
|
666
|
+
reject
|
|
667
|
+
) => {
|
|
668
|
+
server.close(
|
|
669
|
+
(error) => {
|
|
670
|
+
if (error) {
|
|
671
|
+
reject(
|
|
672
|
+
error
|
|
673
|
+
);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
resolve();
|
|
677
|
+
}
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
async function findFreePort(): Promise<number> {
|
|
684
|
+
const server =
|
|
685
|
+
net.createServer();
|
|
686
|
+
|
|
687
|
+
try {
|
|
688
|
+
await new Promise<void>(
|
|
689
|
+
(
|
|
690
|
+
resolve,
|
|
691
|
+
reject
|
|
692
|
+
) => {
|
|
693
|
+
const onError =
|
|
694
|
+
(error: Error) => {
|
|
695
|
+
server.off(
|
|
696
|
+
"listening",
|
|
697
|
+
onListening
|
|
698
|
+
);
|
|
699
|
+
reject(
|
|
700
|
+
error
|
|
701
|
+
);
|
|
702
|
+
};
|
|
703
|
+
const onListening =
|
|
704
|
+
() => {
|
|
705
|
+
server.off(
|
|
706
|
+
"error",
|
|
707
|
+
onError
|
|
708
|
+
);
|
|
709
|
+
resolve();
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
server.once(
|
|
713
|
+
"error",
|
|
714
|
+
onError
|
|
715
|
+
);
|
|
716
|
+
server.once(
|
|
717
|
+
"listening",
|
|
718
|
+
onListening
|
|
719
|
+
);
|
|
720
|
+
server.listen(
|
|
721
|
+
0,
|
|
722
|
+
"127.0.0.1"
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
const address =
|
|
728
|
+
server.address();
|
|
729
|
+
|
|
730
|
+
if (
|
|
731
|
+
!address ||
|
|
732
|
+
typeof address ===
|
|
733
|
+
"string"
|
|
734
|
+
) {
|
|
735
|
+
throw new Error(
|
|
736
|
+
"BCP Framework: could not allocate production action port."
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return address.port;
|
|
741
|
+
} finally {
|
|
742
|
+
if (
|
|
743
|
+
server.listening
|
|
744
|
+
) {
|
|
745
|
+
await new Promise<void>(
|
|
746
|
+
(
|
|
747
|
+
resolve,
|
|
748
|
+
reject
|
|
749
|
+
) => {
|
|
750
|
+
server.close(
|
|
751
|
+
(error) => {
|
|
752
|
+
if (error) {
|
|
753
|
+
reject(
|
|
754
|
+
error
|
|
755
|
+
);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
resolve();
|
|
759
|
+
}
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|