@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,471 @@
1
+ import * as http from "node:http";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import {
5
+ pathToFileURL,
6
+ } from "node:url";
7
+
8
+ import {
9
+ createElement,
10
+ type ComponentType,
11
+ } from "react";
12
+
13
+ import {
14
+ renderToStaticMarkup,
15
+ } from "react-dom/server";
16
+
17
+ import type {
18
+ ResolvedMetadata,
19
+ } from "../../client/src/metadata.js";
20
+
21
+ import {
22
+ matchRoute,
23
+ scanRoutes,
24
+ } from "../../router/src/index.js";
25
+
26
+ import {
27
+ resolveRouteMetadata,
28
+ type MetadataSource,
29
+ } from "./metadata.js";
30
+
31
+ export interface NavigationPayload {
32
+ route: string;
33
+ params: Record<string, string>;
34
+ clientScript: string;
35
+ clientImports: string[];
36
+ title: string;
37
+ metadata: ResolvedMetadata;
38
+ version: number;
39
+ loadingHtml: string | null;
40
+ }
41
+
42
+ export interface NavigationPayloadOptions {
43
+ resolveClientScript?: (
44
+ pathname: string
45
+ ) => string | null;
46
+
47
+ resolveClientImports?: (
48
+ pathname: string
49
+ ) => string[];
50
+
51
+ title?: string;
52
+
53
+ version?: number;
54
+ }
55
+
56
+ export async function resolveNavigationPayload(
57
+ rootDirectory: string,
58
+ pathname: string,
59
+ options: NavigationPayloadOptions = {}
60
+ ): Promise<NavigationPayload | null> {
61
+ const appDirectory =
62
+ path.join(
63
+ rootDirectory,
64
+ "app"
65
+ );
66
+
67
+ const routes =
68
+ scanRoutes(
69
+ appDirectory
70
+ );
71
+
72
+ const match =
73
+ matchRoute(
74
+ routes,
75
+ normalizePathname(
76
+ pathname
77
+ )
78
+ );
79
+
80
+ if (!match) {
81
+ return null;
82
+ }
83
+
84
+ const loadingFile =
85
+ findNearestLoadingFile(
86
+ appDirectory,
87
+ match.route.filePath
88
+ );
89
+
90
+ const loadingHtml =
91
+ loadingFile
92
+ ? await renderLoadingFile(
93
+ loadingFile,
94
+ match.params
95
+ )
96
+ : null;
97
+
98
+ const clientScript =
99
+ options.resolveClientScript
100
+ ? options.resolveClientScript(
101
+ match.route.pathname
102
+ )
103
+ : `/_bcp/client/${routeToBundleName(
104
+ match.route.pathname
105
+ )}`;
106
+
107
+ if (!clientScript) {
108
+ return null;
109
+ }
110
+
111
+ const metadata =
112
+ await resolveDevMetadata(
113
+ [
114
+ ...match.route.layouts,
115
+ match.route.filePath,
116
+ ],
117
+ match.params,
118
+ rootDirectory,
119
+ options.title ??
120
+ "BCP Framework"
121
+ );
122
+
123
+ return {
124
+ route:
125
+ match.route.pathname,
126
+ params:
127
+ match.params,
128
+ clientScript,
129
+ clientImports:
130
+ options.resolveClientImports?.(
131
+ match.route.pathname
132
+ ) ?? [],
133
+ title:
134
+ metadata.title,
135
+ metadata,
136
+ version:
137
+ options.version ??
138
+ Date.now(),
139
+ loadingHtml,
140
+ };
141
+ }
142
+
143
+ export function sendNavigationPayload(
144
+ res: http.ServerResponse,
145
+ payload: NavigationPayload,
146
+ statusCode = 200
147
+ ): void {
148
+ sendJson(
149
+ res,
150
+ statusCode,
151
+ payload
152
+ );
153
+ }
154
+
155
+ export function sendNavigationError(
156
+ res: http.ServerResponse,
157
+ statusCode: number,
158
+ message: string
159
+ ): void {
160
+ sendJson(
161
+ res,
162
+ statusCode,
163
+ {
164
+ error:
165
+ message,
166
+ }
167
+ );
168
+ }
169
+
170
+ async function resolveDevMetadata(
171
+ files: string[],
172
+ params: Record<string, string>,
173
+ rootDirectory: string,
174
+ fallbackTitle: string
175
+ ): Promise<ResolvedMetadata> {
176
+ const sources:
177
+ MetadataSource[] = [];
178
+
179
+ for (const filePath of files) {
180
+ const moduleUrl =
181
+ pathToFileURL(
182
+ filePath
183
+ );
184
+
185
+ moduleUrl.searchParams.set(
186
+ "bcp-metadata",
187
+ String(
188
+ Date.now()
189
+ )
190
+ );
191
+
192
+ const module =
193
+ await import(
194
+ moduleUrl.href
195
+ );
196
+
197
+ sources.push({
198
+ label:
199
+ path.relative(
200
+ rootDirectory,
201
+ filePath
202
+ )
203
+ .replace(
204
+ /\\/g,
205
+ "/"
206
+ ),
207
+ metadata:
208
+ module.metadata,
209
+ generateMetadata:
210
+ module.generateMetadata,
211
+ });
212
+ }
213
+
214
+ return resolveRouteMetadata(
215
+ sources,
216
+ params,
217
+ fallbackTitle
218
+ );
219
+ }
220
+
221
+ async function renderLoadingFile(
222
+ filePath: string,
223
+ params: Record<string, string>
224
+ ): Promise<string> {
225
+ const moduleUrl =
226
+ pathToFileURL(
227
+ filePath
228
+ );
229
+
230
+ moduleUrl.searchParams.set(
231
+ "bcp-loading",
232
+ String(
233
+ Date.now()
234
+ )
235
+ );
236
+
237
+ const loadingModule =
238
+ await import(
239
+ moduleUrl.href
240
+ );
241
+
242
+ const Loading =
243
+ loadingModule.default as
244
+ ComponentType<{
245
+ params: Record<string, string>;
246
+ }>;
247
+
248
+ if (
249
+ typeof Loading !==
250
+ "function"
251
+ ) {
252
+ throw new Error(
253
+ `BCP Framework: loading file "${filePath}" must export a default React component.`
254
+ );
255
+ }
256
+
257
+ return renderToStaticMarkup(
258
+ createElement(
259
+ Loading,
260
+ {
261
+ params,
262
+ }
263
+ )
264
+ );
265
+ }
266
+
267
+ function findNearestLoadingFile(
268
+ appDirectory: string,
269
+ pageFilePath: string
270
+ ): string | null {
271
+ const appRoot =
272
+ path.resolve(
273
+ appDirectory
274
+ );
275
+
276
+ let currentDirectory =
277
+ path.dirname(
278
+ path.resolve(
279
+ pageFilePath
280
+ )
281
+ );
282
+
283
+ while (true) {
284
+ const loadingFile =
285
+ findLoadingFile(
286
+ currentDirectory
287
+ );
288
+
289
+ if (loadingFile) {
290
+ return loadingFile;
291
+ }
292
+
293
+ if (
294
+ currentDirectory ===
295
+ appRoot
296
+ ) {
297
+ break;
298
+ }
299
+
300
+ const parent =
301
+ path.dirname(
302
+ currentDirectory
303
+ );
304
+
305
+ if (
306
+ parent ===
307
+ currentDirectory ||
308
+ !isInsideDirectory(
309
+ appRoot,
310
+ parent
311
+ )
312
+ ) {
313
+ break;
314
+ }
315
+
316
+ currentDirectory =
317
+ parent;
318
+ }
319
+
320
+ return null;
321
+ }
322
+
323
+ function findLoadingFile(
324
+ directory: string
325
+ ): string | null {
326
+ const tsxFile =
327
+ path.join(
328
+ directory,
329
+ "loading.tsx"
330
+ );
331
+
332
+ const tsFile =
333
+ path.join(
334
+ directory,
335
+ "loading.ts"
336
+ );
337
+
338
+ const hasTsx =
339
+ fs.existsSync(
340
+ tsxFile
341
+ );
342
+
343
+ const hasTs =
344
+ fs.existsSync(
345
+ tsFile
346
+ );
347
+
348
+ if (
349
+ hasTsx &&
350
+ hasTs
351
+ ) {
352
+ throw new Error(
353
+ `BCP Framework: both loading.ts and loading.tsx exist in "${directory}".`
354
+ );
355
+ }
356
+
357
+ if (hasTsx) {
358
+ return tsxFile;
359
+ }
360
+
361
+ if (hasTs) {
362
+ return tsFile;
363
+ }
364
+
365
+ return null;
366
+ }
367
+
368
+ function isInsideDirectory(
369
+ rootDirectory: string,
370
+ candidate: string
371
+ ): boolean {
372
+ const relative =
373
+ path.relative(
374
+ rootDirectory,
375
+ candidate
376
+ );
377
+
378
+ return (
379
+ relative === "" ||
380
+ (
381
+ !relative.startsWith("..") &&
382
+ !path.isAbsolute(
383
+ relative
384
+ )
385
+ )
386
+ );
387
+ }
388
+
389
+ function sendJson(
390
+ res: http.ServerResponse,
391
+ statusCode: number,
392
+ value: unknown
393
+ ): void {
394
+ const body =
395
+ JSON.stringify(
396
+ value
397
+ );
398
+
399
+ res.writeHead(
400
+ statusCode,
401
+ {
402
+ "Content-Type":
403
+ "application/json; charset=utf-8",
404
+ "Cache-Control":
405
+ "no-store",
406
+ "X-Content-Type-Options":
407
+ "nosniff",
408
+ "Content-Length":
409
+ Buffer.byteLength(
410
+ body
411
+ ),
412
+ }
413
+ );
414
+
415
+ res.end(
416
+ body
417
+ );
418
+ }
419
+
420
+ function routeToBundleName(
421
+ pathname: string
422
+ ): string {
423
+ if (pathname === "/") {
424
+ return "index.js";
425
+ }
426
+
427
+ const name =
428
+ pathname
429
+ .slice(1)
430
+ .split("/")
431
+ .map(
432
+ sanitizeSegment
433
+ )
434
+ .join("__");
435
+
436
+ return `${name}.js`;
437
+ }
438
+
439
+ function sanitizeSegment(
440
+ segment: string
441
+ ): string {
442
+ const dynamicMatch =
443
+ /^\[([A-Za-z_][A-Za-z0-9_]*)\]$/.exec(
444
+ segment
445
+ );
446
+
447
+ if (dynamicMatch) {
448
+ return `param-${dynamicMatch[1]}`;
449
+ }
450
+
451
+ return segment.replace(
452
+ /[^A-Za-z0-9._-]/g,
453
+ "_"
454
+ );
455
+ }
456
+
457
+ function normalizePathname(
458
+ pathname: string
459
+ ): string {
460
+ if (
461
+ pathname.length > 1 &&
462
+ pathname.endsWith("/")
463
+ ) {
464
+ return pathname.slice(
465
+ 0,
466
+ -1
467
+ );
468
+ }
469
+
470
+ return pathname || "/";
471
+ }