@chidchanun/bcp 0.1.8 → 0.1.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 (29) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +218 -7
  3. package/docs/application-modules.md +63 -3
  4. package/docs/releasing.md +34 -19
  5. package/docs/route-guards.md +240 -0
  6. package/docs/server-data-loaders.md +240 -0
  7. package/docs/updating.md +130 -0
  8. package/package.json +1 -1
  9. package/packages/bundler/src/server-production-guards.ts +497 -0
  10. package/packages/bundler/src/server-production-middleware.ts +33 -8
  11. package/packages/bundler/src/server-production.ts +25 -0
  12. package/packages/cli/src/args.ts +58 -0
  13. package/packages/cli/src/index.ts +41 -13
  14. package/packages/cli/src/update.ts +860 -0
  15. package/packages/client/src/index.tsx +5 -0
  16. package/packages/client/src/loader-data.tsx +229 -0
  17. package/packages/client/src/router-v2.tsx +254 -29
  18. package/packages/server/src/dev-navigation-target.ts +188 -0
  19. package/packages/server/src/index.ts +209 -119
  20. package/packages/server/src/navigation-payload.ts +24 -1
  21. package/packages/server/src/navigation-response.ts +284 -0
  22. package/packages/server/src/page-guard.ts +529 -0
  23. package/packages/server/src/page-loader.ts +643 -0
  24. package/packages/server/src/standalone-production-runtime-v2-guard.ts +815 -0
  25. package/packages/server/src/standalone-production-runtime-v2-navigation.ts +785 -0
  26. package/packages/server/src/standalone-production-runtime-v2.ts +131 -19
  27. package/packages/server/src/standalone-production-runtime-v3.ts +1 -1
  28. package/packages/server/src/standalone-production-runtime-v4.ts +42 -2
  29. package/packages/server/src/static-dev-server.ts +21 -17
@@ -0,0 +1,188 @@
1
+ import * as http from "node:http";
2
+ import path from "node:path";
3
+
4
+ import {
5
+ matchRoute,
6
+ scanRoutes,
7
+ } from "../../router/src/index.js";
8
+
9
+ import {
10
+ resolveNavigationPayload,
11
+ } from "./navigation-payload.js";
12
+
13
+ import {
14
+ attachLoaderDataToParams,
15
+ executePageLoader,
16
+ } from "./page-loader.js";
17
+
18
+ import {
19
+ createNavigationJsonResponse,
20
+ createNavigationRedirectResponse,
21
+ } from "./navigation-response.js";
22
+
23
+ import {
24
+ applyResponseCookies,
25
+ runWithRequestContext,
26
+ } from "./request-context.js";
27
+
28
+ export async function createDevNavigationTargetResponse(
29
+ req: http.IncomingMessage,
30
+ rootDirectory: string,
31
+ target: URL
32
+ ): Promise<Response | null> {
33
+ const routes =
34
+ scanRoutes(
35
+ path.join(
36
+ rootDirectory,
37
+ "app"
38
+ )
39
+ );
40
+
41
+ const match =
42
+ matchRoute(
43
+ routes,
44
+ normalizePathname(
45
+ target.pathname
46
+ )
47
+ );
48
+
49
+ if (!match) {
50
+ return null;
51
+ }
52
+
53
+ const request =
54
+ createNavigationRequest(
55
+ req,
56
+ target
57
+ );
58
+
59
+ return runWithRequestContext(
60
+ request,
61
+ async () => {
62
+ const loaderExecution =
63
+ await executePageLoader(
64
+ match.route.filePath,
65
+ match.params,
66
+ target,
67
+ Date.now()
68
+ );
69
+
70
+ if (
71
+ loaderExecution.response
72
+ ) {
73
+ const loaderResponse =
74
+ applyResponseCookies(
75
+ loaderExecution.response
76
+ );
77
+
78
+ return (
79
+ createNavigationRedirectResponse(
80
+ loaderResponse
81
+ ) ??
82
+ loaderResponse
83
+ );
84
+ }
85
+
86
+ const payload =
87
+ await resolveNavigationPayload(
88
+ rootDirectory,
89
+ target.pathname,
90
+ {
91
+ allowLoaderRoute:
92
+ true,
93
+ }
94
+ );
95
+
96
+ if (!payload) {
97
+ return createNavigationJsonResponse(
98
+ {
99
+ error:
100
+ "Route not found.",
101
+ },
102
+ {
103
+ status:
104
+ 404,
105
+ }
106
+ );
107
+ }
108
+
109
+ payload.params =
110
+ attachLoaderDataToParams(
111
+ match.params,
112
+ loaderExecution
113
+ );
114
+
115
+ return applyResponseCookies(
116
+ createNavigationJsonResponse(
117
+ payload
118
+ )
119
+ );
120
+ },
121
+ {
122
+ remoteAddress:
123
+ req.socket
124
+ .remoteAddress ??
125
+ null,
126
+ }
127
+ );
128
+ }
129
+
130
+ function createNavigationRequest(
131
+ req: http.IncomingMessage,
132
+ target: URL
133
+ ): Request {
134
+ const headers =
135
+ new Headers();
136
+
137
+ for (
138
+ const [key, value]
139
+ of Object.entries(
140
+ req.headers
141
+ )
142
+ ) {
143
+ if (value === undefined) {
144
+ continue;
145
+ }
146
+
147
+ if (Array.isArray(value)) {
148
+ for (const item of value) {
149
+ headers.append(
150
+ key,
151
+ item
152
+ );
153
+ }
154
+ } else {
155
+ headers.set(
156
+ key,
157
+ value
158
+ );
159
+ }
160
+ }
161
+
162
+ return new Request(
163
+ target,
164
+ {
165
+ method:
166
+ "GET",
167
+ headers,
168
+ }
169
+ );
170
+ }
171
+
172
+ function normalizePathname(
173
+ pathname: string
174
+ ): string {
175
+ if (
176
+ pathname.length > 1 &&
177
+ pathname.endsWith(
178
+ "/"
179
+ )
180
+ ) {
181
+ return pathname.slice(
182
+ 0,
183
+ -1
184
+ );
185
+ }
186
+
187
+ return pathname || "/";
188
+ }
@@ -18,6 +18,10 @@ import {
18
18
  renderToString,
19
19
  } from "react-dom/server";
20
20
 
21
+ import {
22
+ BcpLoaderDataProvider,
23
+ } from "../../client/src/loader-data.js";
24
+
21
25
  import {
22
26
  assertNoPageApiConflicts,
23
27
  matchApiRoute,
@@ -41,7 +45,15 @@ import {
41
45
  DevHmrServer,
42
46
  } from "./dev-hmr.js";
43
47
 
44
- import { runWithRequestContext, applyResponseCookies } from "./request-context.ts";
48
+ import {
49
+ attachLoaderDataToParams,
50
+ executePageLoader,
51
+ } from "./page-loader.js";
52
+
53
+ import {
54
+ runWithRequestContext,
55
+ applyResponseCookies,
56
+ } from "./request-context.ts";
45
57
 
46
58
  /*
47
59
  * =====================================
@@ -71,7 +83,7 @@ interface FrameworkData {
71
83
 
72
84
  params: Record<
73
85
  string,
74
- string
86
+ any
75
87
  >;
76
88
  }
77
89
 
@@ -501,124 +513,15 @@ export function createDevServer(
501
513
  );
502
514
  }
503
515
 
504
- /*
505
- * =================================
506
- * Server Components Import
507
- * =================================
508
- */
509
-
510
- const importStartedAt =
511
- performance.now();
512
-
513
- const Page =
514
- await importComponent(
515
- route.filePath,
516
- "Page",
517
- moduleVersion
518
- );
519
-
520
- const Layouts:
521
- ComponentType<any>[] =
522
- [];
523
-
524
- for (
525
- const layoutFile
526
- of route.layouts
527
- ) {
528
- const Layout =
529
- await importComponent(
530
- layoutFile,
531
- "Layout",
532
- moduleVersion
533
- );
534
-
535
- Layouts.push(
536
- Layout
537
- );
538
- }
539
-
540
- console.log(
541
- `Import: ${(performance.now() - importStartedAt).toFixed(2)}ms`
542
- );
543
-
544
- /*
545
- * =================================
546
- * React Tree
547
- * =================================
548
- */
549
-
550
- const renderStartedAt =
551
- performance.now();
552
-
553
- let element:
554
- ReactElement =
555
- createElement(
556
- Page,
557
- {
558
- params,
559
- }
560
- );
561
-
562
- for (
563
- let index =
564
- Layouts.length -
565
- 1;
566
-
567
- index >= 0;
568
-
569
- index--
570
- ) {
571
- const Layout =
572
- Layouts[index];
573
-
574
- element =
575
- createElement(
576
- Layout,
577
- {
578
- params,
579
- },
580
- element
581
- );
582
- }
583
-
584
- const content =
585
- renderToString(
586
- element
587
- );
588
-
589
- console.log(
590
- `SSR: ${(performance.now() - renderStartedAt).toFixed(2)}ms`
591
- );
592
-
593
- /*
594
- * =================================
595
- * Framework Data
596
- * =================================
597
- */
598
-
599
- const frameworkData:
600
- FrameworkData = {
601
- route:
602
- route.pathname,
603
-
604
- params,
605
- };
606
-
607
- const html =
608
- renderDocument(
609
- content,
610
-
611
- pageClientBundle
612
- .publicPath,
613
-
614
- frameworkData,
615
-
616
- clientVersion
617
- );
618
-
619
- sendHtml(
516
+ await handlePageRoute(
517
+ req,
620
518
  res,
621
- html
519
+ requestUrl,
520
+ route,
521
+ params,
522
+ pageClientBundle,
523
+ moduleVersion,
524
+ clientVersion
622
525
  );
623
526
  } catch (error) {
624
527
  console.error("");
@@ -1335,6 +1238,193 @@ export function createDevServer(
1335
1238
  };
1336
1239
  }
1337
1240
 
1241
+ /*
1242
+ * =====================================
1243
+ * Page Route
1244
+ * =====================================
1245
+ */
1246
+
1247
+ async function handlePageRoute(
1248
+ req: http.IncomingMessage,
1249
+ res: http.ServerResponse,
1250
+ requestUrl: URL,
1251
+ route: Route,
1252
+ params: Record<string, any>,
1253
+ pageClientBundle: ClientBundle,
1254
+ moduleVersion: number,
1255
+ clientVersion: number
1256
+ ): Promise<void> {
1257
+ const request =
1258
+ await createWebRequest(
1259
+ req,
1260
+ requestUrl
1261
+ );
1262
+
1263
+ const response =
1264
+ await runWithRequestContext(
1265
+ request,
1266
+ async () => {
1267
+ const loaderExecution =
1268
+ await executePageLoader(
1269
+ route.filePath,
1270
+ params,
1271
+ requestUrl,
1272
+ moduleVersion
1273
+ );
1274
+
1275
+ if (
1276
+ loaderExecution.response
1277
+ ) {
1278
+ return applyResponseCookies(
1279
+ loaderExecution.response
1280
+ );
1281
+ }
1282
+
1283
+ const frameworkParams =
1284
+ attachLoaderDataToParams(
1285
+ params,
1286
+ loaderExecution
1287
+ );
1288
+
1289
+ const importStartedAt =
1290
+ performance.now();
1291
+
1292
+ const Page =
1293
+ await importComponent(
1294
+ route.filePath,
1295
+ "Page",
1296
+ moduleVersion
1297
+ );
1298
+
1299
+ const Layouts:
1300
+ ComponentType<any>[] = [];
1301
+
1302
+ for (
1303
+ const layoutFile
1304
+ of route.layouts
1305
+ ) {
1306
+ const Layout =
1307
+ await importComponent(
1308
+ layoutFile,
1309
+ "Layout",
1310
+ moduleVersion
1311
+ );
1312
+
1313
+ Layouts.push(
1314
+ Layout
1315
+ );
1316
+ }
1317
+
1318
+ console.log(
1319
+ `Import: ${(performance.now() - importStartedAt).toFixed(2)}ms`
1320
+ );
1321
+
1322
+ const renderStartedAt =
1323
+ performance.now();
1324
+
1325
+ let element:
1326
+ ReactElement =
1327
+ createElement(
1328
+ Page,
1329
+ {
1330
+ params:
1331
+ frameworkParams,
1332
+ }
1333
+ );
1334
+
1335
+ for (
1336
+ let index =
1337
+ Layouts.length - 1;
1338
+ index >= 0;
1339
+ index--
1340
+ ) {
1341
+ const Layout =
1342
+ Layouts[index];
1343
+
1344
+ element =
1345
+ createElement(
1346
+ Layout,
1347
+ {
1348
+ params:
1349
+ frameworkParams,
1350
+ },
1351
+ element
1352
+ );
1353
+ }
1354
+
1355
+ if (
1356
+ loaderExecution.hasLoader
1357
+ ) {
1358
+ element =
1359
+ createElement(
1360
+ BcpLoaderDataProvider,
1361
+ {
1362
+ data:
1363
+ loaderExecution.data,
1364
+ },
1365
+ element
1366
+ );
1367
+ }
1368
+
1369
+ const content =
1370
+ renderToString(
1371
+ element
1372
+ );
1373
+
1374
+ console.log(
1375
+ `SSR: ${(performance.now() - renderStartedAt).toFixed(2)}ms`
1376
+ );
1377
+
1378
+ const frameworkData:
1379
+ FrameworkData = {
1380
+ route:
1381
+ route.pathname,
1382
+ params:
1383
+ frameworkParams,
1384
+ };
1385
+
1386
+ const html =
1387
+ renderDocument(
1388
+ content,
1389
+ pageClientBundle
1390
+ .publicPath,
1391
+ frameworkData,
1392
+ clientVersion
1393
+ );
1394
+
1395
+ return applyResponseCookies(
1396
+ new Response(
1397
+ html,
1398
+ {
1399
+ status:
1400
+ 200,
1401
+ headers: {
1402
+ "Content-Type":
1403
+ "text/html; charset=utf-8",
1404
+ "Cache-Control":
1405
+ "no-store",
1406
+ },
1407
+ }
1408
+ )
1409
+ );
1410
+ },
1411
+ {
1412
+ remoteAddress:
1413
+ req.socket
1414
+ .remoteAddress ??
1415
+ null,
1416
+ }
1417
+ );
1418
+
1419
+ await sendWebResponse(
1420
+ res,
1421
+ response,
1422
+ normalizeHttpMethod(
1423
+ req.method
1424
+ ) === "HEAD"
1425
+ );
1426
+ }
1427
+
1338
1428
  /*
1339
1429
  * =====================================
1340
1430
  * API Route
@@ -28,9 +28,13 @@ import {
28
28
  type MetadataSource,
29
29
  } from "./metadata.js";
30
30
 
31
+ import {
32
+ routeHasPageLoader,
33
+ } from "./page-loader.js";
34
+
31
35
  export interface NavigationPayload {
32
36
  route: string;
33
- params: Record<string, string>;
37
+ params: Record<string, unknown>;
34
38
  clientScript: string;
35
39
  clientImports: string[];
36
40
  title: string;
@@ -51,6 +55,8 @@ export interface NavigationPayloadOptions {
51
55
  title?: string;
52
56
 
53
57
  version?: number;
58
+
59
+ allowLoaderRoute?: boolean;
54
60
  }
55
61
 
56
62
  export async function resolveNavigationPayload(
@@ -81,6 +87,23 @@ export async function resolveNavigationPayload(
81
87
  return null;
82
88
  }
83
89
 
90
+ /*
91
+ * A loader-backed route must opt in here because this helper only knows
92
+ * how to build the route/navigation metadata. Step 2 navigation callers
93
+ * execute the loader in a request context first and then attach the fresh
94
+ * loader data to params before sending the payload. Document metadata
95
+ * resolution also opts in because the page loader has already run in the
96
+ * normal SSR request pipeline.
97
+ */
98
+ if (
99
+ !options.allowLoaderRoute &&
100
+ routeHasPageLoader(
101
+ match.route.filePath
102
+ )
103
+ ) {
104
+ return null;
105
+ }
106
+
84
107
  const loadingFile =
85
108
  findNearestLoadingFile(
86
109
  appDirectory,