@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,140 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ pathToFileURL,
5
+ } from "node:url";
6
+
7
+ import type {
8
+ MiddlewareModule,
9
+ } from "./middleware.js";
10
+
11
+ const MIDDLEWARE_EXTENSIONS = [
12
+ ".ts",
13
+ ".tsx",
14
+ ".js",
15
+ ".jsx",
16
+ ] as const;
17
+
18
+ export function findProjectMiddlewareFile(
19
+ rootDirectory: string
20
+ ): string | null {
21
+ const matches =
22
+ MIDDLEWARE_EXTENSIONS
23
+ .map(
24
+ (extension) =>
25
+ path.join(
26
+ rootDirectory,
27
+ `middleware${extension}`
28
+ )
29
+ )
30
+ .filter(
31
+ (filePath) =>
32
+ fs.existsSync(
33
+ filePath
34
+ ) &&
35
+ fs.statSync(
36
+ filePath
37
+ ).isFile()
38
+ );
39
+
40
+ if (
41
+ matches.length > 1
42
+ ) {
43
+ throw new Error(
44
+ `BCP Framework: multiple middleware files found: ${matches.map((value) => path.basename(value)).join(", ")}. Keep only one middleware.ts/.tsx/.js/.jsx file.`
45
+ );
46
+ }
47
+
48
+ return matches[0] ??
49
+ null;
50
+ }
51
+
52
+ export async function loadProjectMiddleware(
53
+ rootDirectory: string
54
+ ): Promise<MiddlewareModule | null> {
55
+ const filePath =
56
+ findProjectMiddlewareFile(
57
+ rootDirectory
58
+ );
59
+
60
+ if (!filePath) {
61
+ return null;
62
+ }
63
+
64
+ const stat =
65
+ fs.statSync(
66
+ filePath
67
+ );
68
+
69
+ const moduleUrl =
70
+ pathToFileURL(
71
+ filePath
72
+ );
73
+
74
+ moduleUrl.searchParams.set(
75
+ "bcp-middleware",
76
+ `${stat.mtimeMs}-${stat.size}`
77
+ );
78
+
79
+ const module =
80
+ await import(
81
+ moduleUrl.href
82
+ ) as MiddlewareModule;
83
+
84
+ assertMiddlewareModule(
85
+ module,
86
+ filePath
87
+ );
88
+
89
+ return module;
90
+ }
91
+
92
+ export function assertMiddlewareModule(
93
+ module: MiddlewareModule,
94
+ filePath = "middleware"
95
+ ): void {
96
+ const handler =
97
+ module.middleware ??
98
+ module.default;
99
+
100
+ if (
101
+ typeof handler !==
102
+ "function"
103
+ ) {
104
+ throw new Error(
105
+ `BCP Framework: ${filePath} must export a middleware(request) function or a default middleware function.`
106
+ );
107
+ }
108
+
109
+ const matcher =
110
+ module.config?.matcher;
111
+
112
+ if (
113
+ matcher === undefined
114
+ ) {
115
+ return;
116
+ }
117
+
118
+ const values =
119
+ Array.isArray(
120
+ matcher
121
+ )
122
+ ? matcher
123
+ : [
124
+ matcher,
125
+ ];
126
+
127
+ for (
128
+ const value
129
+ of values
130
+ ) {
131
+ if (
132
+ typeof value !== "string" &&
133
+ !(value instanceof RegExp)
134
+ ) {
135
+ throw new Error(
136
+ `BCP Framework: middleware matcher in ${filePath} must be a string, RegExp, or an array of them.`
137
+ );
138
+ }
139
+ }
140
+ }
@@ -0,0 +1,516 @@
1
+ import * as http from "node:http";
2
+
3
+ import {
4
+ applyMiddlewareRequestHeaders,
5
+ applyMiddlewareResponseHeaders,
6
+ executeMiddleware,
7
+ sendMiddlewareRedirect,
8
+ sendMiddlewareResponse,
9
+ type MiddlewareModule,
10
+ } from "./middleware.js";
11
+
12
+ import {
13
+ applySecurityHeaders,
14
+ } from "./security.js";
15
+
16
+ export const NAVIGATION_REDIRECT_HEADER =
17
+ "X-BCP-Middleware-Redirect";
18
+
19
+ export interface MiddlewareProxyOptions {
20
+ port: number;
21
+ hostname: string;
22
+ upstreamPort: number;
23
+ upstreamHostname?: string;
24
+ getMiddleware:
25
+ () =>
26
+ MiddlewareModule |
27
+ null |
28
+ Promise<
29
+ MiddlewareModule |
30
+ null
31
+ >;
32
+ }
33
+
34
+ export function createMiddlewareProxy(
35
+ options: MiddlewareProxyOptions
36
+ ) {
37
+ const upstreamHostname =
38
+ options.upstreamHostname ??
39
+ "127.0.0.1";
40
+
41
+ let server:
42
+ http.Server | null =
43
+ null;
44
+
45
+ async function start(): Promise<void> {
46
+ if (server) {
47
+ return;
48
+ }
49
+
50
+ server =
51
+ http.createServer(
52
+ async (
53
+ req,
54
+ res
55
+ ) => {
56
+ applySecurityHeaders(
57
+ res
58
+ );
59
+
60
+ try {
61
+ const host =
62
+ req.headers.host ??
63
+ `${options.hostname}:${options.port}`;
64
+
65
+ const incomingUrl =
66
+ new URL(
67
+ req.url ?? "/",
68
+ `http://${host}`
69
+ );
70
+
71
+ const navigationTarget =
72
+ resolveNavigationTarget(
73
+ incomingUrl
74
+ );
75
+
76
+ const targetUrl =
77
+ navigationTarget ??
78
+ incomingUrl;
79
+
80
+ const module =
81
+ await options
82
+ .getMiddleware();
83
+
84
+ const result =
85
+ await executeMiddleware(
86
+ module,
87
+ {
88
+ url:
89
+ targetUrl,
90
+ method:
91
+ req.method ??
92
+ "GET",
93
+ headers:
94
+ incomingHeadersToHeaders(
95
+ req.headers
96
+ ),
97
+ }
98
+ );
99
+
100
+ if (
101
+ result.type ===
102
+ "redirect"
103
+ ) {
104
+ if (
105
+ navigationTarget
106
+ ) {
107
+ applyMiddlewareResponseHeaders(
108
+ res,
109
+ result.headers
110
+ );
111
+ res.statusCode =
112
+ 204;
113
+ res.setHeader(
114
+ NAVIGATION_REDIRECT_HEADER,
115
+ result.url.href
116
+ );
117
+ res.setHeader(
118
+ "Cache-Control",
119
+ "no-store"
120
+ );
121
+ res.end();
122
+ return;
123
+ }
124
+
125
+ sendMiddlewareRedirect(
126
+ req,
127
+ res,
128
+ result
129
+ );
130
+ return;
131
+ }
132
+
133
+ if (
134
+ result.type ===
135
+ "response"
136
+ ) {
137
+ await sendMiddlewareResponse(
138
+ req,
139
+ res,
140
+ result.response
141
+ );
142
+ return;
143
+ }
144
+
145
+ applyMiddlewareRequestHeaders(
146
+ req,
147
+ result.requestHeaders
148
+ );
149
+ applyMiddlewareResponseHeaders(
150
+ res,
151
+ result.responseHeaders
152
+ );
153
+
154
+ if (
155
+ result.type ===
156
+ "rewrite"
157
+ ) {
158
+ if (
159
+ navigationTarget
160
+ ) {
161
+ incomingUrl
162
+ .searchParams
163
+ .set(
164
+ "url",
165
+ `${result.url.pathname}${result.url.search}${result.url.hash}`
166
+ );
167
+
168
+ req.url =
169
+ `${incomingUrl.pathname}${incomingUrl.search}`;
170
+ } else {
171
+ req.url =
172
+ `${result.url.pathname}${result.url.search}`;
173
+ }
174
+ }
175
+
176
+ proxyRequest(
177
+ req,
178
+ res,
179
+ upstreamHostname,
180
+ options.upstreamPort,
181
+ host
182
+ );
183
+ } catch (error) {
184
+ console.error(
185
+ "[BCP Middleware] request failed:",
186
+ error
187
+ );
188
+
189
+ if (
190
+ res.headersSent
191
+ ) {
192
+ res.destroy();
193
+ return;
194
+ }
195
+
196
+ res.writeHead(
197
+ 500,
198
+ {
199
+ "Content-Type":
200
+ "text/plain; charset=utf-8",
201
+ "Cache-Control":
202
+ "no-store",
203
+ }
204
+ );
205
+ res.end(
206
+ "Internal Server Error"
207
+ );
208
+ }
209
+ }
210
+ );
211
+
212
+ server.keepAliveTimeout =
213
+ 65_000;
214
+ server.headersTimeout =
215
+ 66_000;
216
+
217
+ await listen(
218
+ server,
219
+ options.port,
220
+ options.hostname
221
+ );
222
+ }
223
+
224
+ async function stop(): Promise<void> {
225
+ if (!server) {
226
+ return;
227
+ }
228
+
229
+ const current =
230
+ server;
231
+ server =
232
+ null;
233
+
234
+ await closeServer(
235
+ current
236
+ );
237
+ }
238
+
239
+ return {
240
+ start,
241
+ stop,
242
+ };
243
+ }
244
+
245
+ function resolveNavigationTarget(
246
+ requestUrl: URL
247
+ ): URL | null {
248
+ if (
249
+ requestUrl.pathname !==
250
+ "/_bcp/navigation"
251
+ ) {
252
+ return null;
253
+ }
254
+
255
+ const rawTarget =
256
+ requestUrl.searchParams
257
+ .get(
258
+ "url"
259
+ );
260
+
261
+ if (!rawTarget) {
262
+ return null;
263
+ }
264
+
265
+ try {
266
+ const target =
267
+ new URL(
268
+ rawTarget,
269
+ requestUrl.origin
270
+ );
271
+
272
+ if (
273
+ target.origin !==
274
+ requestUrl.origin
275
+ ) {
276
+ return null;
277
+ }
278
+
279
+ return target;
280
+ } catch {
281
+ return null;
282
+ }
283
+ }
284
+
285
+ function proxyRequest(
286
+ req: http.IncomingMessage,
287
+ res: http.ServerResponse,
288
+ upstreamHostname: string,
289
+ upstreamPort: number,
290
+ originalHost: string
291
+ ): void {
292
+ const upstream =
293
+ http.request(
294
+ {
295
+ hostname:
296
+ upstreamHostname,
297
+ port:
298
+ upstreamPort,
299
+ method:
300
+ req.method ??
301
+ "GET",
302
+ path:
303
+ req.url ??
304
+ "/",
305
+ headers: {
306
+ ...req.headers,
307
+ host:
308
+ originalHost,
309
+ "x-forwarded-host":
310
+ originalHost,
311
+ },
312
+ }
313
+ );
314
+
315
+ upstream.on(
316
+ "response",
317
+ (upstreamResponse) => {
318
+ const headers = {
319
+ ...upstreamResponse.headers,
320
+ };
321
+
322
+ for (
323
+ const [
324
+ name,
325
+ value,
326
+ ]
327
+ of Object.entries(
328
+ headers
329
+ )
330
+ ) {
331
+ if (
332
+ value !== undefined
333
+ ) {
334
+ res.setHeader(
335
+ name,
336
+ value
337
+ );
338
+ }
339
+ }
340
+
341
+ res.statusCode =
342
+ upstreamResponse.statusCode ??
343
+ 502;
344
+
345
+ if (
346
+ upstreamResponse.statusMessage
347
+ ) {
348
+ res.statusMessage =
349
+ upstreamResponse.statusMessage;
350
+ }
351
+
352
+ upstreamResponse.pipe(
353
+ res
354
+ );
355
+ }
356
+ );
357
+
358
+ upstream.on(
359
+ "error",
360
+ (error) => {
361
+ console.error(
362
+ "[BCP Middleware] upstream request failed:",
363
+ error
364
+ );
365
+
366
+ if (
367
+ res.headersSent
368
+ ) {
369
+ res.destroy(
370
+ error
371
+ );
372
+ return;
373
+ }
374
+
375
+ res.writeHead(
376
+ 502,
377
+ {
378
+ "Content-Type":
379
+ "text/plain; charset=utf-8",
380
+ "Cache-Control":
381
+ "no-store",
382
+ }
383
+ );
384
+ res.end(
385
+ "Bad Gateway"
386
+ );
387
+ }
388
+ );
389
+
390
+ req.pipe(
391
+ upstream
392
+ );
393
+ }
394
+
395
+ function incomingHeadersToHeaders(
396
+ input: http.IncomingHttpHeaders
397
+ ): Headers {
398
+ const headers =
399
+ new Headers();
400
+
401
+ for (
402
+ const [
403
+ key,
404
+ value,
405
+ ]
406
+ of Object.entries(
407
+ input
408
+ )
409
+ ) {
410
+ if (
411
+ value === undefined
412
+ ) {
413
+ continue;
414
+ }
415
+
416
+ if (
417
+ Array.isArray(
418
+ value
419
+ )
420
+ ) {
421
+ for (
422
+ const item
423
+ of value
424
+ ) {
425
+ headers.append(
426
+ key,
427
+ item
428
+ );
429
+ }
430
+ continue;
431
+ }
432
+
433
+ headers.set(
434
+ key,
435
+ value
436
+ );
437
+ }
438
+
439
+ return headers;
440
+ }
441
+
442
+ function listen(
443
+ server: http.Server,
444
+ port: number,
445
+ hostname: string
446
+ ): Promise<void> {
447
+ return new Promise(
448
+ (
449
+ resolve,
450
+ reject
451
+ ) => {
452
+ const onError =
453
+ (error: Error) => {
454
+ server.off(
455
+ "listening",
456
+ onListening
457
+ );
458
+ reject(
459
+ error
460
+ );
461
+ };
462
+
463
+ const onListening =
464
+ () => {
465
+ server.off(
466
+ "error",
467
+ onError
468
+ );
469
+ resolve();
470
+ };
471
+
472
+ server.once(
473
+ "error",
474
+ onError
475
+ );
476
+ server.once(
477
+ "listening",
478
+ onListening
479
+ );
480
+ server.listen(
481
+ port,
482
+ hostname
483
+ );
484
+ }
485
+ );
486
+ }
487
+
488
+ function closeServer(
489
+ server: http.Server
490
+ ): Promise<void> {
491
+ if (
492
+ !server.listening
493
+ ) {
494
+ return Promise.resolve();
495
+ }
496
+
497
+ return new Promise(
498
+ (
499
+ resolve,
500
+ reject
501
+ ) => {
502
+ server.close(
503
+ (error) => {
504
+ if (error) {
505
+ reject(
506
+ error
507
+ );
508
+ return;
509
+ }
510
+
511
+ resolve();
512
+ }
513
+ );
514
+ }
515
+ );
516
+ }