@chidchanun/bcp 0.1.0

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 (66) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +241 -0
  4. package/docs/caching.md +76 -0
  5. package/docs/configuration.md +97 -0
  6. package/docs/deployment.md +74 -0
  7. package/docs/getting-started.md +82 -0
  8. package/docs/middleware.md +58 -0
  9. package/docs/releasing.md +299 -0
  10. package/docs/routing.md +103 -0
  11. package/docs/security.md +57 -0
  12. package/package.json +68 -0
  13. package/packages/bundler/src/client-islands.ts +1457 -0
  14. package/packages/bundler/src/incremental-context.ts +206 -0
  15. package/packages/bundler/src/index.ts +1991 -0
  16. package/packages/bundler/src/module-graph.ts +317 -0
  17. package/packages/bundler/src/partial-hydration.ts +414 -0
  18. package/packages/bundler/src/production.ts +974 -0
  19. package/packages/bundler/src/server-production-middleware.ts +447 -0
  20. package/packages/bundler/src/server-production.ts +1193 -0
  21. package/packages/bundler/src/special-files.ts +131 -0
  22. package/packages/cache/src/index.ts +761 -0
  23. package/packages/cli/bin/bcp.mjs +93 -0
  24. package/packages/cli/src/args.ts +305 -0
  25. package/packages/cli/src/bootstrap.ts +514 -0
  26. package/packages/cli/src/index.ts +504 -0
  27. package/packages/cli/src/version.ts +45 -0
  28. package/packages/client/src/cache.ts +11 -0
  29. package/packages/client/src/config.ts +18 -0
  30. package/packages/client/src/error-boundary.tsx +149 -0
  31. package/packages/client/src/hydration.ts +3 -0
  32. package/packages/client/src/index.tsx +57 -0
  33. package/packages/client/src/islands.tsx +315 -0
  34. package/packages/client/src/metadata.ts +281 -0
  35. package/packages/client/src/navigation-loading.ts +52 -0
  36. package/packages/client/src/navigation-state.ts +80 -0
  37. package/packages/client/src/not-found.ts +34 -0
  38. package/packages/client/src/persistent-layout-runtime.ts +273 -0
  39. package/packages/client/src/router-v2.tsx +969 -0
  40. package/packages/client/src/router.tsx +1 -0
  41. package/packages/config/src/index.ts +1038 -0
  42. package/packages/env/src/index.ts +593 -0
  43. package/packages/router/src/advanced-router.ts +1032 -0
  44. package/packages/router/src/index.ts +1 -0
  45. package/packages/server/src/compression.ts +249 -0
  46. package/packages/server/src/dev-document-metadata.ts +154 -0
  47. package/packages/server/src/dev-hmr.ts +225 -0
  48. package/packages/server/src/index.ts +2265 -0
  49. package/packages/server/src/metadata.ts +478 -0
  50. package/packages/server/src/middleware-dev-server.ts +260 -0
  51. package/packages/server/src/middleware-loader.ts +140 -0
  52. package/packages/server/src/middleware-proxy.ts +516 -0
  53. package/packages/server/src/middleware.ts +704 -0
  54. package/packages/server/src/navigation-payload.ts +471 -0
  55. package/packages/server/src/production-server.ts +1746 -0
  56. package/packages/server/src/response-cache-proxy.ts +828 -0
  57. package/packages/server/src/security-proxy.ts +406 -0
  58. package/packages/server/src/security.ts +451 -0
  59. package/packages/server/src/standalone-production-runtime-v2.ts +2047 -0
  60. package/packages/server/src/standalone-production-runtime-v3.ts +250 -0
  61. package/packages/server/src/standalone-production-runtime-v4.ts +289 -0
  62. package/packages/server/src/standalone-production-runtime-v5.ts +289 -0
  63. package/packages/server/src/standalone-production-runtime.ts +1951 -0
  64. package/packages/server/src/standalone-production-server.ts +6 -0
  65. package/packages/server/src/static-assets.ts +262 -0
  66. package/packages/server/src/static-dev-server.ts +854 -0
@@ -0,0 +1,854 @@
1
+ import * as http from "node:http";
2
+ import * as net from "node:net";
3
+ import path from "node:path";
4
+
5
+ import {
6
+ createDevServer,
7
+ type DevServerOptions,
8
+ } from "./index.js";
9
+
10
+ import {
11
+ applyDevDocumentMetadata,
12
+ } from "./dev-document-metadata.js";
13
+
14
+ import {
15
+ resolveNavigationPayload,
16
+ sendNavigationError,
17
+ sendNavigationPayload,
18
+ } from "./navigation-payload.js";
19
+
20
+ import {
21
+ readPublicAsset,
22
+ type StaticAsset,
23
+ } from "./static-assets.js";
24
+
25
+ export function createStaticDevServer(
26
+ options: DevServerOptions = {}
27
+ ) {
28
+ const port =
29
+ options.port ?? 3000;
30
+
31
+ const hostname =
32
+ options.hostname ??
33
+ "localhost";
34
+
35
+ const rootDirectory =
36
+ path.resolve(
37
+ options.rootDirectory ??
38
+ process.cwd()
39
+ );
40
+
41
+ let internalPort = 0;
42
+
43
+ let frameworkServer:
44
+ ReturnType<typeof createDevServer> | null =
45
+ null;
46
+
47
+ let proxyServer:
48
+ http.Server | null =
49
+ null;
50
+
51
+ let started = false;
52
+
53
+ async function start() {
54
+ if (started) {
55
+ return;
56
+ }
57
+
58
+ internalPort =
59
+ await findFreePort();
60
+
61
+ frameworkServer =
62
+ createDevServer({
63
+ ...options,
64
+ rootDirectory,
65
+ hostname:
66
+ "127.0.0.1",
67
+ port:
68
+ internalPort,
69
+ });
70
+
71
+ await frameworkServer.start();
72
+
73
+ proxyServer =
74
+ http.createServer(
75
+ async (
76
+ req,
77
+ res
78
+ ) => {
79
+ const startedAt =
80
+ performance.now();
81
+
82
+ try {
83
+ const requestUrl =
84
+ new URL(
85
+ req.url ?? "/",
86
+ `http://${req.headers.host ?? `${hostname}:${port}`}`
87
+ );
88
+
89
+ const pathname =
90
+ requestUrl.pathname;
91
+
92
+ if (
93
+ pathname ===
94
+ "/_bcp/navigation"
95
+ ) {
96
+ await handleNavigationRequest(
97
+ req,
98
+ res,
99
+ requestUrl,
100
+ rootDirectory,
101
+ startedAt
102
+ );
103
+
104
+ return;
105
+ }
106
+
107
+ if (
108
+ isFrameworkPath(
109
+ pathname
110
+ )
111
+ ) {
112
+ proxyRequest(
113
+ req,
114
+ res
115
+ );
116
+
117
+ return;
118
+ }
119
+
120
+ const asset =
121
+ await readPublicAsset(
122
+ rootDirectory,
123
+ pathname
124
+ );
125
+
126
+ if (!asset) {
127
+ proxyRequest(
128
+ req,
129
+ res,
130
+ pathname
131
+ );
132
+
133
+ return;
134
+ }
135
+
136
+ sendStaticAsset(
137
+ req,
138
+ res,
139
+ asset
140
+ );
141
+
142
+ logRequest(
143
+ req,
144
+ pathname,
145
+ res,
146
+ startedAt
147
+ );
148
+ } catch (error) {
149
+ console.error(
150
+ "[public] request failed:",
151
+ error
152
+ );
153
+
154
+ if (
155
+ !res.headersSent
156
+ ) {
157
+ res.writeHead(
158
+ 500,
159
+ {
160
+ "Content-Type":
161
+ "text/plain; charset=utf-8",
162
+ "Cache-Control":
163
+ "no-store",
164
+ }
165
+ );
166
+
167
+ res.end(
168
+ "Internal Server Error"
169
+ );
170
+ } else {
171
+ res.destroy();
172
+ }
173
+ }
174
+ }
175
+ );
176
+
177
+ proxyServer.keepAliveTimeout =
178
+ 65_000;
179
+
180
+ proxyServer.headersTimeout =
181
+ 66_000;
182
+
183
+ try {
184
+ await listen(
185
+ proxyServer,
186
+ port,
187
+ hostname
188
+ );
189
+ } catch (error) {
190
+ await frameworkServer.stop();
191
+
192
+ frameworkServer = null;
193
+ proxyServer = null;
194
+
195
+ throw error;
196
+ }
197
+
198
+ started = true;
199
+
200
+ console.log("");
201
+ console.log(
202
+ "BCP Framework"
203
+ );
204
+ console.log(
205
+ `Local: http://${hostname}:${port}`
206
+ );
207
+ console.log(
208
+ `Public: ${path.join(rootDirectory, "public")}`
209
+ );
210
+ console.log("");
211
+ }
212
+
213
+ async function stop() {
214
+ if (proxyServer) {
215
+ await closeServer(
216
+ proxyServer
217
+ );
218
+
219
+ proxyServer = null;
220
+ }
221
+
222
+ if (frameworkServer) {
223
+ await frameworkServer.stop();
224
+
225
+ frameworkServer = null;
226
+ }
227
+
228
+ started = false;
229
+ }
230
+
231
+ function proxyRequest(
232
+ req: http.IncomingMessage,
233
+ res: http.ServerResponse,
234
+ metadataPathname?: string
235
+ ) {
236
+ const upstream =
237
+ http.request({
238
+ hostname:
239
+ "127.0.0.1",
240
+ port:
241
+ internalPort,
242
+ method:
243
+ req.method ??
244
+ "GET",
245
+ path:
246
+ req.url ??
247
+ "/",
248
+ headers: {
249
+ ...req.headers,
250
+ host:
251
+ req.headers.host ??
252
+ `${hostname}:${port}`,
253
+ "x-forwarded-host":
254
+ req.headers.host ??
255
+ `${hostname}:${port}`,
256
+ },
257
+ });
258
+
259
+ upstream.on(
260
+ "response",
261
+ (upstreamResponse) => {
262
+ if (
263
+ metadataPathname &&
264
+ shouldTransformHtml(
265
+ req,
266
+ upstreamResponse
267
+ )
268
+ ) {
269
+ void sendHtmlWithMetadata(
270
+ res,
271
+ upstreamResponse,
272
+ rootDirectory,
273
+ metadataPathname
274
+ );
275
+
276
+ return;
277
+ }
278
+
279
+ writeUpstreamHead(
280
+ res,
281
+ upstreamResponse
282
+ );
283
+
284
+ upstreamResponse.pipe(
285
+ res
286
+ );
287
+ }
288
+ );
289
+
290
+ upstream.on(
291
+ "error",
292
+ (error) => {
293
+ console.error(
294
+ "[proxy] request failed:",
295
+ error
296
+ );
297
+
298
+ if (
299
+ res.headersSent
300
+ ) {
301
+ res.destroy(
302
+ error
303
+ );
304
+
305
+ return;
306
+ }
307
+
308
+ res.writeHead(
309
+ 502,
310
+ {
311
+ "Content-Type":
312
+ "text/plain; charset=utf-8",
313
+ "Cache-Control":
314
+ "no-store",
315
+ }
316
+ );
317
+
318
+ res.end(
319
+ "Bad Gateway"
320
+ );
321
+ }
322
+ );
323
+
324
+ req.pipe(
325
+ upstream
326
+ );
327
+ }
328
+
329
+ return {
330
+ start,
331
+ stop,
332
+ };
333
+ }
334
+
335
+ function shouldTransformHtml(
336
+ req: http.IncomingMessage,
337
+ response: http.IncomingMessage
338
+ ): boolean {
339
+ if (
340
+ (req.method ?? "GET").toUpperCase() !==
341
+ "GET"
342
+ ) {
343
+ return false;
344
+ }
345
+
346
+ if (
347
+ response.statusCode !==
348
+ 200
349
+ ) {
350
+ return false;
351
+ }
352
+
353
+ const contentType =
354
+ response.headers[
355
+ "content-type"
356
+ ];
357
+
358
+ const value =
359
+ Array.isArray(
360
+ contentType
361
+ )
362
+ ? contentType[0]
363
+ : contentType;
364
+
365
+ return (
366
+ typeof value === "string" &&
367
+ /^text\/html\b/i.test(
368
+ value
369
+ )
370
+ );
371
+ }
372
+
373
+ async function sendHtmlWithMetadata(
374
+ res: http.ServerResponse,
375
+ upstreamResponse: http.IncomingMessage,
376
+ rootDirectory: string,
377
+ pathname: string
378
+ ): Promise<void> {
379
+ try {
380
+ const chunks:
381
+ Buffer[] = [];
382
+
383
+ for await (
384
+ const chunk
385
+ of upstreamResponse
386
+ ) {
387
+ chunks.push(
388
+ Buffer.isBuffer(
389
+ chunk
390
+ )
391
+ ? chunk
392
+ : Buffer.from(
393
+ chunk
394
+ )
395
+ );
396
+ }
397
+
398
+ const originalHtml =
399
+ Buffer.concat(
400
+ chunks
401
+ ).toString(
402
+ "utf8"
403
+ );
404
+
405
+ const payload =
406
+ await resolveNavigationPayload(
407
+ rootDirectory,
408
+ pathname
409
+ );
410
+
411
+ const html =
412
+ payload
413
+ ? applyDevDocumentMetadata(
414
+ originalHtml,
415
+ payload.metadata
416
+ )
417
+ : originalHtml;
418
+
419
+ const headers:
420
+ http.OutgoingHttpHeaders = {
421
+ ...upstreamResponse.headers,
422
+ };
423
+
424
+ delete headers[
425
+ "content-length"
426
+ ];
427
+ delete headers[
428
+ "transfer-encoding"
429
+ ];
430
+
431
+ headers[
432
+ "content-length"
433
+ ] =
434
+ Buffer.byteLength(
435
+ html
436
+ );
437
+
438
+ writeUpstreamHead(
439
+ res,
440
+ upstreamResponse,
441
+ headers
442
+ );
443
+
444
+ res.end(
445
+ html
446
+ );
447
+ } catch (error) {
448
+ console.error(
449
+ "[dev-metadata] initial SSR metadata failed:",
450
+ error
451
+ );
452
+
453
+ if (
454
+ res.headersSent
455
+ ) {
456
+ res.destroy();
457
+ return;
458
+ }
459
+
460
+ res.writeHead(
461
+ 500,
462
+ {
463
+ "Content-Type":
464
+ "text/plain; charset=utf-8",
465
+ "Cache-Control":
466
+ "no-store",
467
+ }
468
+ );
469
+
470
+ res.end(
471
+ "Internal Server Error"
472
+ );
473
+ }
474
+ }
475
+
476
+ function writeUpstreamHead(
477
+ res: http.ServerResponse,
478
+ upstreamResponse: http.IncomingMessage,
479
+ headers:
480
+ http.OutgoingHttpHeaders =
481
+ upstreamResponse.headers
482
+ ): void {
483
+ const statusCode =
484
+ upstreamResponse.statusCode ??
485
+ 502;
486
+
487
+ if (
488
+ upstreamResponse.statusMessage
489
+ ) {
490
+ res.writeHead(
491
+ statusCode,
492
+ upstreamResponse.statusMessage,
493
+ headers
494
+ );
495
+
496
+ return;
497
+ }
498
+
499
+ res.writeHead(
500
+ statusCode,
501
+ headers
502
+ );
503
+ }
504
+
505
+ async function handleNavigationRequest(
506
+ req: http.IncomingMessage,
507
+ res: http.ServerResponse,
508
+ requestUrl: URL,
509
+ rootDirectory: string,
510
+ startedAt: number
511
+ ): Promise<void> {
512
+ if (
513
+ req.method !== "GET"
514
+ ) {
515
+ sendNavigationError(
516
+ res,
517
+ 405,
518
+ "Method Not Allowed"
519
+ );
520
+
521
+ logRequest(
522
+ req,
523
+ requestUrl.pathname,
524
+ res,
525
+ startedAt
526
+ );
527
+
528
+ return;
529
+ }
530
+
531
+ const targetPath =
532
+ requestUrl.searchParams.get(
533
+ "url"
534
+ );
535
+
536
+ if (!targetPath) {
537
+ sendNavigationError(
538
+ res,
539
+ 400,
540
+ "Missing navigation target."
541
+ );
542
+
543
+ logRequest(
544
+ req,
545
+ requestUrl.pathname,
546
+ res,
547
+ startedAt
548
+ );
549
+
550
+ return;
551
+ }
552
+
553
+ let target: URL;
554
+
555
+ try {
556
+ target =
557
+ new URL(
558
+ targetPath,
559
+ requestUrl.origin
560
+ );
561
+ } catch {
562
+ sendNavigationError(
563
+ res,
564
+ 400,
565
+ "Invalid navigation target."
566
+ );
567
+
568
+ logRequest(
569
+ req,
570
+ requestUrl.pathname,
571
+ res,
572
+ startedAt
573
+ );
574
+
575
+ return;
576
+ }
577
+
578
+ if (
579
+ target.origin !==
580
+ requestUrl.origin
581
+ ) {
582
+ sendNavigationError(
583
+ res,
584
+ 400,
585
+ "Cross-origin navigation is not allowed."
586
+ );
587
+
588
+ logRequest(
589
+ req,
590
+ requestUrl.pathname,
591
+ res,
592
+ startedAt
593
+ );
594
+
595
+ return;
596
+ }
597
+
598
+ const payload =
599
+ await resolveNavigationPayload(
600
+ rootDirectory,
601
+ target.pathname
602
+ );
603
+
604
+ if (!payload) {
605
+ sendNavigationError(
606
+ res,
607
+ 404,
608
+ "Route not found."
609
+ );
610
+
611
+ logRequest(
612
+ req,
613
+ requestUrl.pathname,
614
+ res,
615
+ startedAt
616
+ );
617
+
618
+ return;
619
+ }
620
+
621
+ sendNavigationPayload(
622
+ res,
623
+ payload
624
+ );
625
+
626
+ logRequest(
627
+ req,
628
+ requestUrl.pathname,
629
+ res,
630
+ startedAt
631
+ );
632
+ }
633
+
634
+ function logRequest(
635
+ req: http.IncomingMessage,
636
+ pathname: string,
637
+ res: http.ServerResponse,
638
+ startedAt: number
639
+ ): void {
640
+ console.log(
641
+ `${req.method ?? "GET"} ${pathname} ${res.statusCode} ${(performance.now() - startedAt).toFixed(2)}ms`
642
+ );
643
+ }
644
+
645
+ function isFrameworkPath(
646
+ pathname: string
647
+ ): boolean {
648
+ return (
649
+ pathname === "/api" ||
650
+ pathname.startsWith("/api/") ||
651
+ pathname === "/_bcp" ||
652
+ pathname.startsWith("/_bcp/")
653
+ );
654
+ }
655
+
656
+ function sendStaticAsset(
657
+ req: http.IncomingMessage,
658
+ res: http.ServerResponse,
659
+ asset: StaticAsset
660
+ ): void {
661
+ const ifNoneMatch =
662
+ req.headers[
663
+ "if-none-match"
664
+ ];
665
+
666
+ const ifNoneMatchValue =
667
+ Array.isArray(
668
+ ifNoneMatch
669
+ )
670
+ ? ifNoneMatch.join(",")
671
+ : ifNoneMatch;
672
+
673
+ const ifModifiedSince =
674
+ req.headers[
675
+ "if-modified-since"
676
+ ];
677
+
678
+ const etagMatches =
679
+ ifNoneMatchValue === "*" ||
680
+ ifNoneMatchValue
681
+ ?.split(",")
682
+ .map(
683
+ (value) =>
684
+ value.trim()
685
+ )
686
+ .includes(
687
+ asset.etag
688
+ );
689
+
690
+ const modifiedSinceMatches =
691
+ ifModifiedSince &&
692
+ Number.isFinite(
693
+ Date.parse(
694
+ ifModifiedSince
695
+ )
696
+ ) &&
697
+ Date.parse(
698
+ asset.lastModified
699
+ ) <=
700
+ Date.parse(
701
+ ifModifiedSince
702
+ );
703
+
704
+ const commonHeaders = {
705
+ "Cache-Control":
706
+ "public, max-age=0, must-revalidate",
707
+ ETag:
708
+ asset.etag,
709
+ "Last-Modified":
710
+ asset.lastModified,
711
+ "X-Content-Type-Options":
712
+ "nosniff",
713
+ };
714
+
715
+ if (
716
+ etagMatches ||
717
+ modifiedSinceMatches
718
+ ) {
719
+ res.writeHead(
720
+ 304,
721
+ commonHeaders
722
+ );
723
+
724
+ res.end();
725
+
726
+ return;
727
+ }
728
+
729
+ res.writeHead(
730
+ 200,
731
+ {
732
+ ...commonHeaders,
733
+ "Content-Type":
734
+ asset.contentType,
735
+ "Content-Length":
736
+ asset.content.length,
737
+ }
738
+ );
739
+
740
+ if (
741
+ req.method === "HEAD"
742
+ ) {
743
+ res.end();
744
+
745
+ return;
746
+ }
747
+
748
+ res.end(
749
+ asset.content
750
+ );
751
+ }
752
+
753
+ async function findFreePort(): Promise<number> {
754
+ const server =
755
+ net.createServer();
756
+
757
+ try {
758
+ await listen(
759
+ server,
760
+ 0,
761
+ "127.0.0.1"
762
+ );
763
+
764
+ const address =
765
+ server.address();
766
+
767
+ if (
768
+ !address ||
769
+ typeof address === "string"
770
+ ) {
771
+ throw new Error(
772
+ "Could not determine an available TCP port."
773
+ );
774
+ }
775
+
776
+ return address.port;
777
+ } finally {
778
+ await closeServer(
779
+ server
780
+ );
781
+ }
782
+ }
783
+
784
+ function listen(
785
+ server: net.Server | http.Server,
786
+ port: number,
787
+ hostname: string
788
+ ): Promise<void> {
789
+ return new Promise(
790
+ (
791
+ resolve,
792
+ reject
793
+ ) => {
794
+ const onError =
795
+ (error: Error) => {
796
+ server.off(
797
+ "listening",
798
+ onListening
799
+ );
800
+ reject(error);
801
+ };
802
+
803
+ const onListening =
804
+ () => {
805
+ server.off(
806
+ "error",
807
+ onError
808
+ );
809
+ resolve();
810
+ };
811
+
812
+ server.once(
813
+ "error",
814
+ onError
815
+ );
816
+ server.once(
817
+ "listening",
818
+ onListening
819
+ );
820
+ server.listen(
821
+ port,
822
+ hostname
823
+ );
824
+ }
825
+ );
826
+ }
827
+
828
+ function closeServer(
829
+ server: net.Server | http.Server
830
+ ): Promise<void> {
831
+ if (
832
+ !server.listening
833
+ ) {
834
+ return Promise.resolve();
835
+ }
836
+
837
+ return new Promise(
838
+ (
839
+ resolve,
840
+ reject
841
+ ) => {
842
+ server.close(
843
+ (error) => {
844
+ if (error) {
845
+ reject(error);
846
+ return;
847
+ }
848
+
849
+ resolve();
850
+ }
851
+ );
852
+ }
853
+ );
854
+ }