@chidchanun/bcp 0.1.9 → 0.1.11
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 +83 -1
- package/README.md +263 -8
- package/docs/form-actions.md +314 -0
- package/docs/releasing.md +34 -19
- package/docs/route-guards.md +240 -0
- package/docs/updating.md +130 -0
- package/package.json +1 -1
- package/packages/bundler/src/server-production-actions.ts +523 -0
- package/packages/bundler/src/server-production-guards.ts +497 -0
- package/packages/bundler/src/server-production-middleware.ts +50 -8
- package/packages/cli/src/args.ts +58 -0
- package/packages/cli/src/index.ts +41 -13
- package/packages/cli/src/update.ts +860 -0
- package/packages/client/src/form.tsx +548 -0
- package/packages/client/src/index.tsx +11 -0
- package/packages/client/src/loader-data.tsx +171 -41
- 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/page-guard.ts +529 -0
- package/packages/server/src/page-loader.ts +317 -20
- package/packages/server/src/standalone-production-runtime-v2-action.ts +765 -0
- package/packages/server/src/standalone-production-runtime-v2-guard.ts +815 -0
- package/packages/server/src/standalone-production-runtime-v3.ts +1 -1
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
import * as http from "node:http";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
runWithRequestCacheContext,
|
|
6
|
+
} from "../../cache/src/index.js";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
matchRoute,
|
|
10
|
+
scanRoutes,
|
|
11
|
+
} from "../../router/src/index.js";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
executePageAction,
|
|
15
|
+
normalizePageActionMethod,
|
|
16
|
+
} from "./form-action.js";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
completePageActionExecution,
|
|
20
|
+
createFormActionErrorResponse,
|
|
21
|
+
createFormActionTransportResponse,
|
|
22
|
+
isEnhancedFormActionRequest,
|
|
23
|
+
} from "./form-action-transport.js";
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
sendNavigationWebResponse,
|
|
27
|
+
} from "./navigation-response.js";
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
runWithRequestContext,
|
|
31
|
+
} from "./request-context.js";
|
|
32
|
+
|
|
33
|
+
export interface FormActionDevProxyOptions {
|
|
34
|
+
port: number;
|
|
35
|
+
hostname: string;
|
|
36
|
+
upstreamPort: number;
|
|
37
|
+
upstreamHostname?: string;
|
|
38
|
+
rootDirectory: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createFormActionDevProxy(
|
|
42
|
+
options: FormActionDevProxyOptions
|
|
43
|
+
) {
|
|
44
|
+
const upstreamHostname =
|
|
45
|
+
options.upstreamHostname ??
|
|
46
|
+
"127.0.0.1";
|
|
47
|
+
const rootDirectory =
|
|
48
|
+
path.resolve(
|
|
49
|
+
options.rootDirectory
|
|
50
|
+
);
|
|
51
|
+
const appDirectory =
|
|
52
|
+
path.join(
|
|
53
|
+
rootDirectory,
|
|
54
|
+
"app"
|
|
55
|
+
);
|
|
56
|
+
let server:
|
|
57
|
+
http.Server | null =
|
|
58
|
+
null;
|
|
59
|
+
|
|
60
|
+
async function start(): Promise<void> {
|
|
61
|
+
if (server) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
server =
|
|
66
|
+
http.createServer(
|
|
67
|
+
async (
|
|
68
|
+
req,
|
|
69
|
+
res
|
|
70
|
+
) => {
|
|
71
|
+
try {
|
|
72
|
+
const requestUrl =
|
|
73
|
+
new URL(
|
|
74
|
+
req.url ?? "/",
|
|
75
|
+
`http://${req.headers.host ?? `${options.hostname}:${options.port}`}`
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
requestUrl.pathname !==
|
|
80
|
+
"/_bcp/action"
|
|
81
|
+
) {
|
|
82
|
+
proxyRequest(
|
|
83
|
+
req,
|
|
84
|
+
res,
|
|
85
|
+
upstreamHostname,
|
|
86
|
+
options.upstreamPort
|
|
87
|
+
);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await handleActionRequest(
|
|
92
|
+
req,
|
|
93
|
+
res,
|
|
94
|
+
requestUrl,
|
|
95
|
+
appDirectory
|
|
96
|
+
);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error(
|
|
99
|
+
"[BCP Action Error]",
|
|
100
|
+
error
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (
|
|
104
|
+
res.headersSent
|
|
105
|
+
) {
|
|
106
|
+
res.destroy();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await sendNavigationWebResponse(
|
|
111
|
+
req,
|
|
112
|
+
res,
|
|
113
|
+
createFormActionErrorResponse(
|
|
114
|
+
"Internal Server Error",
|
|
115
|
+
500
|
|
116
|
+
)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
server.keepAliveTimeout =
|
|
123
|
+
65_000;
|
|
124
|
+
server.headersTimeout =
|
|
125
|
+
66_000;
|
|
126
|
+
|
|
127
|
+
await listen(
|
|
128
|
+
server,
|
|
129
|
+
options.port,
|
|
130
|
+
options.hostname
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function stop(): Promise<void> {
|
|
135
|
+
if (!server) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const current =
|
|
140
|
+
server;
|
|
141
|
+
server =
|
|
142
|
+
null;
|
|
143
|
+
|
|
144
|
+
await closeServer(
|
|
145
|
+
current
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
start,
|
|
151
|
+
stop,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function handleActionRequest(
|
|
156
|
+
req: http.IncomingMessage,
|
|
157
|
+
res: http.ServerResponse,
|
|
158
|
+
requestUrl: URL,
|
|
159
|
+
appDirectory: string
|
|
160
|
+
): Promise<void> {
|
|
161
|
+
if (
|
|
162
|
+
(req.method ?? "GET")
|
|
163
|
+
.toUpperCase() !==
|
|
164
|
+
"POST"
|
|
165
|
+
) {
|
|
166
|
+
await sendNavigationWebResponse(
|
|
167
|
+
req,
|
|
168
|
+
res,
|
|
169
|
+
createFormActionErrorResponse(
|
|
170
|
+
"Method Not Allowed",
|
|
171
|
+
405
|
|
172
|
+
)
|
|
173
|
+
);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const actionName =
|
|
178
|
+
requestUrl.searchParams.get(
|
|
179
|
+
"name"
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (!actionName) {
|
|
183
|
+
await sendNavigationWebResponse(
|
|
184
|
+
req,
|
|
185
|
+
res,
|
|
186
|
+
createFormActionErrorResponse(
|
|
187
|
+
"Missing form action name.",
|
|
188
|
+
400
|
|
189
|
+
)
|
|
190
|
+
);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const target =
|
|
195
|
+
resolveActionTarget(
|
|
196
|
+
req,
|
|
197
|
+
requestUrl
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
if (!target) {
|
|
201
|
+
await sendNavigationWebResponse(
|
|
202
|
+
req,
|
|
203
|
+
res,
|
|
204
|
+
createFormActionErrorResponse(
|
|
205
|
+
"Missing or invalid form action target.",
|
|
206
|
+
400
|
|
207
|
+
)
|
|
208
|
+
);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const method =
|
|
213
|
+
normalizePageActionMethod(
|
|
214
|
+
requestUrl.searchParams.get(
|
|
215
|
+
"method"
|
|
216
|
+
)
|
|
217
|
+
);
|
|
218
|
+
const routes =
|
|
219
|
+
scanRoutes(
|
|
220
|
+
appDirectory
|
|
221
|
+
);
|
|
222
|
+
const match =
|
|
223
|
+
matchRoute(
|
|
224
|
+
routes,
|
|
225
|
+
target.pathname
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
if (!match) {
|
|
229
|
+
await sendNavigationWebResponse(
|
|
230
|
+
req,
|
|
231
|
+
res,
|
|
232
|
+
createFormActionErrorResponse(
|
|
233
|
+
"Form action route not found.",
|
|
234
|
+
404
|
|
235
|
+
)
|
|
236
|
+
);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const formData =
|
|
241
|
+
await readFormData(
|
|
242
|
+
req,
|
|
243
|
+
requestUrl
|
|
244
|
+
);
|
|
245
|
+
const actionRequest =
|
|
246
|
+
new Request(
|
|
247
|
+
target,
|
|
248
|
+
{
|
|
249
|
+
method,
|
|
250
|
+
headers:
|
|
251
|
+
collectHeaders(
|
|
252
|
+
req
|
|
253
|
+
),
|
|
254
|
+
}
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const completed =
|
|
258
|
+
await runWithRequestContext(
|
|
259
|
+
actionRequest,
|
|
260
|
+
() =>
|
|
261
|
+
runWithRequestCacheContext(
|
|
262
|
+
target.pathname,
|
|
263
|
+
async () => {
|
|
264
|
+
const execution =
|
|
265
|
+
await executePageAction(
|
|
266
|
+
match.route.filePath,
|
|
267
|
+
actionName,
|
|
268
|
+
method,
|
|
269
|
+
formData,
|
|
270
|
+
match.params,
|
|
271
|
+
target,
|
|
272
|
+
Date.now()
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
return completePageActionExecution(
|
|
276
|
+
execution
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
),
|
|
280
|
+
{
|
|
281
|
+
remoteAddress:
|
|
282
|
+
req.socket
|
|
283
|
+
.remoteAddress ??
|
|
284
|
+
null,
|
|
285
|
+
}
|
|
286
|
+
);
|
|
287
|
+
const response =
|
|
288
|
+
createFormActionTransportResponse(
|
|
289
|
+
completed,
|
|
290
|
+
target,
|
|
291
|
+
isEnhancedFormActionRequest(
|
|
292
|
+
collectHeaders(
|
|
293
|
+
req
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
await sendNavigationWebResponse(
|
|
299
|
+
req,
|
|
300
|
+
res,
|
|
301
|
+
response
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function resolveActionTarget(
|
|
306
|
+
req: http.IncomingMessage,
|
|
307
|
+
requestUrl: URL
|
|
308
|
+
): URL | null {
|
|
309
|
+
const candidates = [
|
|
310
|
+
requestUrl.searchParams.get(
|
|
311
|
+
"url"
|
|
312
|
+
),
|
|
313
|
+
headerValue(
|
|
314
|
+
req.headers[
|
|
315
|
+
"x-bcp-action-target"
|
|
316
|
+
]
|
|
317
|
+
),
|
|
318
|
+
headerValue(
|
|
319
|
+
req.headers.referer
|
|
320
|
+
),
|
|
321
|
+
];
|
|
322
|
+
|
|
323
|
+
for (
|
|
324
|
+
const candidate
|
|
325
|
+
of candidates
|
|
326
|
+
) {
|
|
327
|
+
if (!candidate) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
const target =
|
|
333
|
+
new URL(
|
|
334
|
+
candidate,
|
|
335
|
+
requestUrl.origin
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
if (
|
|
339
|
+
target.origin ===
|
|
340
|
+
requestUrl.origin &&
|
|
341
|
+
target.pathname !==
|
|
342
|
+
"/_bcp/action"
|
|
343
|
+
) {
|
|
344
|
+
return target;
|
|
345
|
+
}
|
|
346
|
+
} catch {
|
|
347
|
+
// Try the next target source.
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function readFormData(
|
|
355
|
+
req: http.IncomingMessage,
|
|
356
|
+
requestUrl: URL
|
|
357
|
+
): Promise<FormData> {
|
|
358
|
+
const chunks:
|
|
359
|
+
Buffer[] = [];
|
|
360
|
+
|
|
361
|
+
for await (
|
|
362
|
+
const chunk
|
|
363
|
+
of req
|
|
364
|
+
) {
|
|
365
|
+
chunks.push(
|
|
366
|
+
Buffer.isBuffer(
|
|
367
|
+
chunk
|
|
368
|
+
)
|
|
369
|
+
? chunk
|
|
370
|
+
: Buffer.from(
|
|
371
|
+
chunk
|
|
372
|
+
)
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const body =
|
|
377
|
+
Buffer.concat(
|
|
378
|
+
chunks
|
|
379
|
+
);
|
|
380
|
+
const request =
|
|
381
|
+
new Request(
|
|
382
|
+
requestUrl,
|
|
383
|
+
{
|
|
384
|
+
method: "POST",
|
|
385
|
+
headers:
|
|
386
|
+
collectHeaders(
|
|
387
|
+
req
|
|
388
|
+
),
|
|
389
|
+
body:
|
|
390
|
+
body.length > 0
|
|
391
|
+
? body
|
|
392
|
+
: undefined,
|
|
393
|
+
}
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
return request.formData();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function collectHeaders(
|
|
400
|
+
req: http.IncomingMessage
|
|
401
|
+
): Headers {
|
|
402
|
+
const headers =
|
|
403
|
+
new Headers();
|
|
404
|
+
|
|
405
|
+
for (
|
|
406
|
+
const [
|
|
407
|
+
name,
|
|
408
|
+
value,
|
|
409
|
+
]
|
|
410
|
+
of Object.entries(
|
|
411
|
+
req.headers
|
|
412
|
+
)
|
|
413
|
+
) {
|
|
414
|
+
if (
|
|
415
|
+
value === undefined
|
|
416
|
+
) {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (
|
|
421
|
+
Array.isArray(
|
|
422
|
+
value
|
|
423
|
+
)
|
|
424
|
+
) {
|
|
425
|
+
for (
|
|
426
|
+
const item
|
|
427
|
+
of value
|
|
428
|
+
) {
|
|
429
|
+
headers.append(
|
|
430
|
+
name,
|
|
431
|
+
item
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
headers.set(
|
|
436
|
+
name,
|
|
437
|
+
value
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return headers;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function headerValue(
|
|
446
|
+
value: string | string[] | undefined
|
|
447
|
+
): string | null {
|
|
448
|
+
if (
|
|
449
|
+
Array.isArray(
|
|
450
|
+
value
|
|
451
|
+
)
|
|
452
|
+
) {
|
|
453
|
+
return value[0] ??
|
|
454
|
+
null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return value ??
|
|
458
|
+
null;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function proxyRequest(
|
|
462
|
+
req: http.IncomingMessage,
|
|
463
|
+
res: http.ServerResponse,
|
|
464
|
+
upstreamHostname: string,
|
|
465
|
+
upstreamPort: number
|
|
466
|
+
): void {
|
|
467
|
+
const upstream =
|
|
468
|
+
http.request({
|
|
469
|
+
hostname:
|
|
470
|
+
upstreamHostname,
|
|
471
|
+
port:
|
|
472
|
+
upstreamPort,
|
|
473
|
+
method:
|
|
474
|
+
req.method ??
|
|
475
|
+
"GET",
|
|
476
|
+
path:
|
|
477
|
+
req.url ??
|
|
478
|
+
"/",
|
|
479
|
+
headers:
|
|
480
|
+
req.headers,
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
upstream.on(
|
|
484
|
+
"response",
|
|
485
|
+
(upstreamResponse) => {
|
|
486
|
+
res.writeHead(
|
|
487
|
+
upstreamResponse.statusCode ??
|
|
488
|
+
502,
|
|
489
|
+
upstreamResponse.headers
|
|
490
|
+
);
|
|
491
|
+
upstreamResponse.pipe(
|
|
492
|
+
res
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
upstream.on(
|
|
498
|
+
"error",
|
|
499
|
+
(error) => {
|
|
500
|
+
if (
|
|
501
|
+
res.headersSent
|
|
502
|
+
) {
|
|
503
|
+
res.destroy(
|
|
504
|
+
error
|
|
505
|
+
);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
res.writeHead(
|
|
510
|
+
502,
|
|
511
|
+
{
|
|
512
|
+
"Content-Type":
|
|
513
|
+
"text/plain; charset=utf-8",
|
|
514
|
+
"Cache-Control":
|
|
515
|
+
"no-store",
|
|
516
|
+
}
|
|
517
|
+
);
|
|
518
|
+
res.end(
|
|
519
|
+
"Bad Gateway"
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
req.pipe(
|
|
525
|
+
upstream
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function listen(
|
|
530
|
+
server: http.Server,
|
|
531
|
+
port: number,
|
|
532
|
+
hostname: string
|
|
533
|
+
): Promise<void> {
|
|
534
|
+
return new Promise(
|
|
535
|
+
(
|
|
536
|
+
resolve,
|
|
537
|
+
reject
|
|
538
|
+
) => {
|
|
539
|
+
const onError =
|
|
540
|
+
(error: Error) => {
|
|
541
|
+
server.off(
|
|
542
|
+
"listening",
|
|
543
|
+
onListening
|
|
544
|
+
);
|
|
545
|
+
reject(
|
|
546
|
+
error
|
|
547
|
+
);
|
|
548
|
+
};
|
|
549
|
+
const onListening =
|
|
550
|
+
() => {
|
|
551
|
+
server.off(
|
|
552
|
+
"error",
|
|
553
|
+
onError
|
|
554
|
+
);
|
|
555
|
+
resolve();
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
server.once(
|
|
559
|
+
"error",
|
|
560
|
+
onError
|
|
561
|
+
);
|
|
562
|
+
server.once(
|
|
563
|
+
"listening",
|
|
564
|
+
onListening
|
|
565
|
+
);
|
|
566
|
+
server.listen(
|
|
567
|
+
port,
|
|
568
|
+
hostname
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function closeServer(
|
|
575
|
+
server: http.Server
|
|
576
|
+
): Promise<void> {
|
|
577
|
+
if (
|
|
578
|
+
!server.listening
|
|
579
|
+
) {
|
|
580
|
+
return Promise.resolve();
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return new Promise(
|
|
584
|
+
(
|
|
585
|
+
resolve,
|
|
586
|
+
reject
|
|
587
|
+
) => {
|
|
588
|
+
server.close(
|
|
589
|
+
(error) => {
|
|
590
|
+
if (error) {
|
|
591
|
+
reject(
|
|
592
|
+
error
|
|
593
|
+
);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
resolve();
|
|
597
|
+
}
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
);
|
|
601
|
+
}
|