@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.
- package/CHANGELOG.md +49 -0
- package/LICENSE +21 -0
- package/README.md +241 -0
- package/docs/caching.md +76 -0
- package/docs/configuration.md +97 -0
- package/docs/deployment.md +74 -0
- package/docs/getting-started.md +82 -0
- package/docs/middleware.md +58 -0
- package/docs/releasing.md +299 -0
- package/docs/routing.md +103 -0
- package/docs/security.md +57 -0
- package/package.json +68 -0
- package/packages/bundler/src/client-islands.ts +1457 -0
- package/packages/bundler/src/incremental-context.ts +206 -0
- package/packages/bundler/src/index.ts +1991 -0
- package/packages/bundler/src/module-graph.ts +317 -0
- package/packages/bundler/src/partial-hydration.ts +414 -0
- package/packages/bundler/src/production.ts +974 -0
- package/packages/bundler/src/server-production-middleware.ts +447 -0
- package/packages/bundler/src/server-production.ts +1193 -0
- package/packages/bundler/src/special-files.ts +131 -0
- package/packages/cache/src/index.ts +761 -0
- package/packages/cli/bin/bcp.mjs +93 -0
- package/packages/cli/src/args.ts +305 -0
- package/packages/cli/src/bootstrap.ts +514 -0
- package/packages/cli/src/index.ts +504 -0
- package/packages/cli/src/version.ts +45 -0
- package/packages/client/src/cache.ts +11 -0
- package/packages/client/src/config.ts +18 -0
- package/packages/client/src/error-boundary.tsx +149 -0
- package/packages/client/src/hydration.ts +3 -0
- package/packages/client/src/index.tsx +57 -0
- package/packages/client/src/islands.tsx +315 -0
- package/packages/client/src/metadata.ts +281 -0
- package/packages/client/src/navigation-loading.ts +52 -0
- package/packages/client/src/navigation-state.ts +80 -0
- package/packages/client/src/not-found.ts +34 -0
- package/packages/client/src/persistent-layout-runtime.ts +273 -0
- package/packages/client/src/router-v2.tsx +969 -0
- package/packages/client/src/router.tsx +1 -0
- package/packages/config/src/index.ts +1038 -0
- package/packages/env/src/index.ts +593 -0
- package/packages/router/src/advanced-router.ts +1032 -0
- package/packages/router/src/index.ts +1 -0
- package/packages/server/src/compression.ts +249 -0
- package/packages/server/src/dev-document-metadata.ts +154 -0
- package/packages/server/src/dev-hmr.ts +225 -0
- package/packages/server/src/index.ts +2265 -0
- package/packages/server/src/metadata.ts +478 -0
- package/packages/server/src/middleware-dev-server.ts +260 -0
- package/packages/server/src/middleware-loader.ts +140 -0
- package/packages/server/src/middleware-proxy.ts +516 -0
- package/packages/server/src/middleware.ts +704 -0
- package/packages/server/src/navigation-payload.ts +471 -0
- package/packages/server/src/production-server.ts +1746 -0
- package/packages/server/src/response-cache-proxy.ts +828 -0
- package/packages/server/src/security-proxy.ts +406 -0
- package/packages/server/src/security.ts +451 -0
- package/packages/server/src/standalone-production-runtime-v2.ts +2047 -0
- package/packages/server/src/standalone-production-runtime-v3.ts +250 -0
- package/packages/server/src/standalone-production-runtime-v4.ts +289 -0
- package/packages/server/src/standalone-production-runtime-v5.ts +289 -0
- package/packages/server/src/standalone-production-runtime.ts +1951 -0
- package/packages/server/src/standalone-production-server.ts +6 -0
- package/packages/server/src/static-assets.ts +262 -0
- package/packages/server/src/static-dev-server.ts +854 -0
|
@@ -0,0 +1,1746 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import * as http from "node:http";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
pathToFileURL,
|
|
6
|
+
} from "node:url";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
createElement,
|
|
10
|
+
type ComponentType,
|
|
11
|
+
type ReactElement,
|
|
12
|
+
} from "react";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
renderToString,
|
|
16
|
+
} from "react-dom/server";
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
ProductionBuildManifest,
|
|
20
|
+
ProductionRouteAsset,
|
|
21
|
+
} from "../../bundler/src/production.js";
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
assertNoPageApiConflicts,
|
|
25
|
+
matchApiRoute,
|
|
26
|
+
matchRoute,
|
|
27
|
+
scanApiRoutes,
|
|
28
|
+
scanRoutes,
|
|
29
|
+
type ApiRoute,
|
|
30
|
+
} from "../../router/src/index.js";
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
compressDynamicContent,
|
|
34
|
+
isCompressibleContentType,
|
|
35
|
+
selectContentEncoding,
|
|
36
|
+
type ContentEncoding,
|
|
37
|
+
} from "./compression.js";
|
|
38
|
+
|
|
39
|
+
import {
|
|
40
|
+
resolveNavigationPayload,
|
|
41
|
+
sendNavigationError,
|
|
42
|
+
sendNavigationPayload,
|
|
43
|
+
} from "./navigation-payload.js";
|
|
44
|
+
|
|
45
|
+
import {
|
|
46
|
+
getContentType,
|
|
47
|
+
readPublicAsset,
|
|
48
|
+
type StaticAsset,
|
|
49
|
+
} from "./static-assets.js";
|
|
50
|
+
|
|
51
|
+
export interface ProductionServerOptions {
|
|
52
|
+
port?: number;
|
|
53
|
+
hostname?: string;
|
|
54
|
+
rootDirectory?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface FrameworkData {
|
|
58
|
+
route: string;
|
|
59
|
+
params: Record<string, string>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface ApiContext {
|
|
63
|
+
params: Record<string, string>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type ApiHandler = (
|
|
67
|
+
request: Request,
|
|
68
|
+
context: ApiContext
|
|
69
|
+
) => Response | Promise<Response>;
|
|
70
|
+
|
|
71
|
+
const HTTP_METHODS = [
|
|
72
|
+
"GET",
|
|
73
|
+
"POST",
|
|
74
|
+
"PUT",
|
|
75
|
+
"PATCH",
|
|
76
|
+
"DELETE",
|
|
77
|
+
"HEAD",
|
|
78
|
+
"OPTIONS",
|
|
79
|
+
] as const;
|
|
80
|
+
|
|
81
|
+
type HttpMethod =
|
|
82
|
+
typeof HTTP_METHODS[number];
|
|
83
|
+
|
|
84
|
+
export function createProductionServer(
|
|
85
|
+
options: ProductionServerOptions = {}
|
|
86
|
+
) {
|
|
87
|
+
const port =
|
|
88
|
+
options.port ??
|
|
89
|
+
3000;
|
|
90
|
+
|
|
91
|
+
const hostname =
|
|
92
|
+
options.hostname ??
|
|
93
|
+
"localhost";
|
|
94
|
+
|
|
95
|
+
const rootDirectory =
|
|
96
|
+
path.resolve(
|
|
97
|
+
options.rootDirectory ??
|
|
98
|
+
process.cwd()
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const appDirectory =
|
|
102
|
+
path.join(
|
|
103
|
+
rootDirectory,
|
|
104
|
+
"app"
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const buildDirectory =
|
|
108
|
+
path.join(
|
|
109
|
+
rootDirectory,
|
|
110
|
+
".bcp-framework",
|
|
111
|
+
"build"
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const clientDirectory =
|
|
115
|
+
path.join(
|
|
116
|
+
buildDirectory,
|
|
117
|
+
"client"
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const manifestPath =
|
|
121
|
+
path.join(
|
|
122
|
+
buildDirectory,
|
|
123
|
+
"manifest.json"
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
let manifest:
|
|
127
|
+
ProductionBuildManifest | null =
|
|
128
|
+
null;
|
|
129
|
+
|
|
130
|
+
const routes =
|
|
131
|
+
scanRoutes(
|
|
132
|
+
appDirectory
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const apiRoutes =
|
|
136
|
+
scanApiRoutes(
|
|
137
|
+
appDirectory
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
assertNoPageApiConflicts(
|
|
141
|
+
routes,
|
|
142
|
+
apiRoutes
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const server =
|
|
146
|
+
http.createServer(
|
|
147
|
+
async (
|
|
148
|
+
req,
|
|
149
|
+
res
|
|
150
|
+
) => {
|
|
151
|
+
const startedAt =
|
|
152
|
+
performance.now();
|
|
153
|
+
|
|
154
|
+
let pathname =
|
|
155
|
+
"/";
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const requestUrl =
|
|
159
|
+
new URL(
|
|
160
|
+
req.url ?? "/",
|
|
161
|
+
`http://${
|
|
162
|
+
req.headers.host ??
|
|
163
|
+
`${hostname}:${port}`
|
|
164
|
+
}`
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
pathname =
|
|
168
|
+
normalizePathname(
|
|
169
|
+
requestUrl.pathname
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
res.once(
|
|
173
|
+
"finish",
|
|
174
|
+
() => {
|
|
175
|
+
const duration =
|
|
176
|
+
performance.now() -
|
|
177
|
+
startedAt;
|
|
178
|
+
|
|
179
|
+
console.log(
|
|
180
|
+
`${req.method ?? "GET"} ${pathname} ${res.statusCode} ${duration.toFixed(2)}ms`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
if (
|
|
186
|
+
pathname ===
|
|
187
|
+
"/_bcp/navigation"
|
|
188
|
+
) {
|
|
189
|
+
await handleNavigationRequest(
|
|
190
|
+
req,
|
|
191
|
+
res,
|
|
192
|
+
requestUrl,
|
|
193
|
+
rootDirectory,
|
|
194
|
+
requireManifest(
|
|
195
|
+
manifest
|
|
196
|
+
)
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (
|
|
203
|
+
pathname ===
|
|
204
|
+
"/_bcp/dev-runtime.js"
|
|
205
|
+
) {
|
|
206
|
+
sendJavaScript(
|
|
207
|
+
res,
|
|
208
|
+
Buffer.from(
|
|
209
|
+
"/* BCP production: HMR disabled */"
|
|
210
|
+
),
|
|
211
|
+
false
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (
|
|
218
|
+
pathname.startsWith(
|
|
219
|
+
"/_bcp/client/"
|
|
220
|
+
)
|
|
221
|
+
) {
|
|
222
|
+
const served =
|
|
223
|
+
await serveBuildAsset(
|
|
224
|
+
req,
|
|
225
|
+
res,
|
|
226
|
+
clientDirectory,
|
|
227
|
+
pathname
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
if (!served) {
|
|
231
|
+
sendNotFound(
|
|
232
|
+
res
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const apiMatch =
|
|
240
|
+
matchApiRoute(
|
|
241
|
+
apiRoutes,
|
|
242
|
+
pathname
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
if (apiMatch) {
|
|
246
|
+
await handleApiRoute(
|
|
247
|
+
req,
|
|
248
|
+
res,
|
|
249
|
+
requestUrl,
|
|
250
|
+
apiMatch.route,
|
|
251
|
+
apiMatch.params,
|
|
252
|
+
requireManifest(
|
|
253
|
+
manifest
|
|
254
|
+
).buildId
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const publicAsset =
|
|
261
|
+
await readPublicAsset(
|
|
262
|
+
rootDirectory,
|
|
263
|
+
pathname
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
if (publicAsset) {
|
|
267
|
+
sendPublicAsset(
|
|
268
|
+
req,
|
|
269
|
+
res,
|
|
270
|
+
publicAsset
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const match =
|
|
277
|
+
matchRoute(
|
|
278
|
+
routes,
|
|
279
|
+
pathname
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
if (!match) {
|
|
283
|
+
sendNotFound(
|
|
284
|
+
res
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const activeManifest =
|
|
291
|
+
requireManifest(
|
|
292
|
+
manifest
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const routeAsset =
|
|
296
|
+
activeManifest.routes[
|
|
297
|
+
match.route.pathname
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
if (!routeAsset) {
|
|
301
|
+
throw new Error(
|
|
302
|
+
`BCP Framework: production bundle was not found for route "${match.route.pathname}". Run bcp build again.`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const Page =
|
|
307
|
+
await importComponent(
|
|
308
|
+
match.route.filePath,
|
|
309
|
+
"Page",
|
|
310
|
+
activeManifest.buildId
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const Layouts:
|
|
314
|
+
ComponentType<any>[] =
|
|
315
|
+
[];
|
|
316
|
+
|
|
317
|
+
for (
|
|
318
|
+
const layoutFile
|
|
319
|
+
of match.route.layouts
|
|
320
|
+
) {
|
|
321
|
+
Layouts.push(
|
|
322
|
+
await importComponent(
|
|
323
|
+
layoutFile,
|
|
324
|
+
"Layout",
|
|
325
|
+
activeManifest.buildId
|
|
326
|
+
)
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
let element:
|
|
331
|
+
ReactElement =
|
|
332
|
+
createElement(
|
|
333
|
+
Page,
|
|
334
|
+
{
|
|
335
|
+
params:
|
|
336
|
+
match.params,
|
|
337
|
+
}
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
for (
|
|
341
|
+
let index =
|
|
342
|
+
Layouts.length - 1;
|
|
343
|
+
index >= 0;
|
|
344
|
+
index--
|
|
345
|
+
) {
|
|
346
|
+
element =
|
|
347
|
+
createElement(
|
|
348
|
+
Layouts[index],
|
|
349
|
+
{
|
|
350
|
+
params:
|
|
351
|
+
match.params,
|
|
352
|
+
},
|
|
353
|
+
element
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const content =
|
|
358
|
+
renderToString(
|
|
359
|
+
element
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
const frameworkData:
|
|
363
|
+
FrameworkData = {
|
|
364
|
+
route:
|
|
365
|
+
match.route.pathname,
|
|
366
|
+
params:
|
|
367
|
+
match.params,
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
sendHtml(
|
|
371
|
+
req,
|
|
372
|
+
res,
|
|
373
|
+
renderDocument(
|
|
374
|
+
content,
|
|
375
|
+
routeAsset,
|
|
376
|
+
frameworkData
|
|
377
|
+
)
|
|
378
|
+
);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
console.error(
|
|
381
|
+
"BCP Production Error:",
|
|
382
|
+
error
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
if (
|
|
386
|
+
res.headersSent
|
|
387
|
+
) {
|
|
388
|
+
res.end();
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
sendInternalServerError(
|
|
393
|
+
res,
|
|
394
|
+
error
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
async start() {
|
|
402
|
+
process.env.NODE_ENV =
|
|
403
|
+
"production";
|
|
404
|
+
|
|
405
|
+
manifest =
|
|
406
|
+
await readManifest(
|
|
407
|
+
manifestPath
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
await new Promise<void>(
|
|
411
|
+
(
|
|
412
|
+
resolve,
|
|
413
|
+
reject
|
|
414
|
+
) => {
|
|
415
|
+
const onError =
|
|
416
|
+
(error: Error) => {
|
|
417
|
+
server.off(
|
|
418
|
+
"listening",
|
|
419
|
+
onListening
|
|
420
|
+
);
|
|
421
|
+
reject(error);
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const onListening =
|
|
425
|
+
() => {
|
|
426
|
+
server.off(
|
|
427
|
+
"error",
|
|
428
|
+
onError
|
|
429
|
+
);
|
|
430
|
+
resolve();
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
server.once(
|
|
434
|
+
"error",
|
|
435
|
+
onError
|
|
436
|
+
);
|
|
437
|
+
server.once(
|
|
438
|
+
"listening",
|
|
439
|
+
onListening
|
|
440
|
+
);
|
|
441
|
+
server.listen(
|
|
442
|
+
port,
|
|
443
|
+
hostname
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
console.log("");
|
|
449
|
+
console.log(
|
|
450
|
+
"BCP Framework Production"
|
|
451
|
+
);
|
|
452
|
+
console.log(
|
|
453
|
+
`Local: http://${hostname}:${port}`
|
|
454
|
+
);
|
|
455
|
+
console.log(
|
|
456
|
+
`Build: ${manifest.buildId}`
|
|
457
|
+
);
|
|
458
|
+
console.log("");
|
|
459
|
+
},
|
|
460
|
+
|
|
461
|
+
async stop() {
|
|
462
|
+
if (!server.listening) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
await new Promise<void>(
|
|
467
|
+
(
|
|
468
|
+
resolve,
|
|
469
|
+
reject
|
|
470
|
+
) => {
|
|
471
|
+
server.close(
|
|
472
|
+
(error) => {
|
|
473
|
+
if (error) {
|
|
474
|
+
reject(error);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
resolve();
|
|
479
|
+
}
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
);
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function readManifest(
|
|
488
|
+
manifestPath: string
|
|
489
|
+
): Promise<ProductionBuildManifest> {
|
|
490
|
+
let content: string;
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
content =
|
|
494
|
+
await fs.readFile(
|
|
495
|
+
manifestPath,
|
|
496
|
+
"utf8"
|
|
497
|
+
);
|
|
498
|
+
} catch {
|
|
499
|
+
throw new Error(
|
|
500
|
+
"BCP Framework: production build was not found. Run `bcp build` before `bcp start`."
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const manifest =
|
|
505
|
+
JSON.parse(
|
|
506
|
+
content
|
|
507
|
+
) as
|
|
508
|
+
Partial<ProductionBuildManifest>;
|
|
509
|
+
|
|
510
|
+
if (
|
|
511
|
+
typeof manifest.version !==
|
|
512
|
+
"number" ||
|
|
513
|
+
typeof manifest.buildId !==
|
|
514
|
+
"string" ||
|
|
515
|
+
typeof manifest.builtAt !==
|
|
516
|
+
"string" ||
|
|
517
|
+
!manifest.routes ||
|
|
518
|
+
typeof manifest.routes !==
|
|
519
|
+
"object" ||
|
|
520
|
+
!Array.isArray(
|
|
521
|
+
manifest.assets
|
|
522
|
+
) ||
|
|
523
|
+
!Object.values(
|
|
524
|
+
manifest.routes
|
|
525
|
+
).every(
|
|
526
|
+
isValidRouteAsset
|
|
527
|
+
)
|
|
528
|
+
) {
|
|
529
|
+
throw new Error(
|
|
530
|
+
"BCP Framework: production manifest is invalid. Run `bcp build` again."
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return manifest as
|
|
535
|
+
ProductionBuildManifest;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function isValidRouteAsset(
|
|
539
|
+
value: unknown
|
|
540
|
+
): value is ProductionRouteAsset {
|
|
541
|
+
if (
|
|
542
|
+
!value ||
|
|
543
|
+
typeof value !== "object"
|
|
544
|
+
) {
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const route =
|
|
549
|
+
value as
|
|
550
|
+
Partial<ProductionRouteAsset>;
|
|
551
|
+
|
|
552
|
+
return (
|
|
553
|
+
typeof route.entry ===
|
|
554
|
+
"string" &&
|
|
555
|
+
Array.isArray(
|
|
556
|
+
route.imports
|
|
557
|
+
) &&
|
|
558
|
+
route.imports.every(
|
|
559
|
+
(item) =>
|
|
560
|
+
typeof item ===
|
|
561
|
+
"string"
|
|
562
|
+
) &&
|
|
563
|
+
Boolean(
|
|
564
|
+
route.size
|
|
565
|
+
) &&
|
|
566
|
+
typeof route.size?.raw ===
|
|
567
|
+
"number" &&
|
|
568
|
+
typeof route.size?.gzip ===
|
|
569
|
+
"number" &&
|
|
570
|
+
typeof route.size?.brotli ===
|
|
571
|
+
"number"
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function requireManifest(
|
|
576
|
+
manifest:
|
|
577
|
+
ProductionBuildManifest | null
|
|
578
|
+
): ProductionBuildManifest {
|
|
579
|
+
if (!manifest) {
|
|
580
|
+
throw new Error(
|
|
581
|
+
"BCP Framework: production server has not loaded the build manifest."
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return manifest;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async function handleNavigationRequest(
|
|
589
|
+
req: http.IncomingMessage,
|
|
590
|
+
res: http.ServerResponse,
|
|
591
|
+
requestUrl: URL,
|
|
592
|
+
rootDirectory: string,
|
|
593
|
+
manifest: ProductionBuildManifest
|
|
594
|
+
): Promise<void> {
|
|
595
|
+
if (
|
|
596
|
+
req.method !== "GET"
|
|
597
|
+
) {
|
|
598
|
+
sendNavigationError(
|
|
599
|
+
res,
|
|
600
|
+
405,
|
|
601
|
+
"Method Not Allowed"
|
|
602
|
+
);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const targetPath =
|
|
607
|
+
requestUrl.searchParams.get(
|
|
608
|
+
"url"
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
if (!targetPath) {
|
|
612
|
+
sendNavigationError(
|
|
613
|
+
res,
|
|
614
|
+
400,
|
|
615
|
+
"Missing navigation target."
|
|
616
|
+
);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
let target: URL;
|
|
621
|
+
|
|
622
|
+
try {
|
|
623
|
+
target =
|
|
624
|
+
new URL(
|
|
625
|
+
targetPath,
|
|
626
|
+
requestUrl.origin
|
|
627
|
+
);
|
|
628
|
+
} catch {
|
|
629
|
+
sendNavigationError(
|
|
630
|
+
res,
|
|
631
|
+
400,
|
|
632
|
+
"Invalid navigation target."
|
|
633
|
+
);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (
|
|
638
|
+
target.origin !==
|
|
639
|
+
requestUrl.origin
|
|
640
|
+
) {
|
|
641
|
+
sendNavigationError(
|
|
642
|
+
res,
|
|
643
|
+
400,
|
|
644
|
+
"Cross-origin navigation is not allowed."
|
|
645
|
+
);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const payload =
|
|
650
|
+
await resolveNavigationPayload(
|
|
651
|
+
rootDirectory,
|
|
652
|
+
target.pathname,
|
|
653
|
+
{
|
|
654
|
+
resolveClientScript:
|
|
655
|
+
(pathname) =>
|
|
656
|
+
manifest.routes[
|
|
657
|
+
pathname
|
|
658
|
+
]?.entry ??
|
|
659
|
+
null,
|
|
660
|
+
resolveClientImports:
|
|
661
|
+
(pathname) =>
|
|
662
|
+
manifest.routes[
|
|
663
|
+
pathname
|
|
664
|
+
]?.imports ??
|
|
665
|
+
[],
|
|
666
|
+
version:
|
|
667
|
+
manifest.version,
|
|
668
|
+
}
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
if (!payload) {
|
|
672
|
+
sendNavigationError(
|
|
673
|
+
res,
|
|
674
|
+
404,
|
|
675
|
+
"Route not found."
|
|
676
|
+
);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
sendNavigationPayload(
|
|
681
|
+
res,
|
|
682
|
+
payload
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async function serveBuildAsset(
|
|
687
|
+
req: http.IncomingMessage,
|
|
688
|
+
res: http.ServerResponse,
|
|
689
|
+
clientDirectory: string,
|
|
690
|
+
pathname: string
|
|
691
|
+
): Promise<boolean> {
|
|
692
|
+
const encodedRelative =
|
|
693
|
+
pathname.slice(
|
|
694
|
+
"/_bcp/client/".length
|
|
695
|
+
);
|
|
696
|
+
|
|
697
|
+
let relativePath: string;
|
|
698
|
+
|
|
699
|
+
try {
|
|
700
|
+
relativePath =
|
|
701
|
+
decodeURIComponent(
|
|
702
|
+
encodedRelative
|
|
703
|
+
);
|
|
704
|
+
} catch {
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (
|
|
709
|
+
!relativePath ||
|
|
710
|
+
relativePath.includes("\0")
|
|
711
|
+
) {
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const root =
|
|
716
|
+
path.resolve(
|
|
717
|
+
clientDirectory
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
const candidate =
|
|
721
|
+
path.resolve(
|
|
722
|
+
root,
|
|
723
|
+
relativePath.replaceAll(
|
|
724
|
+
"/",
|
|
725
|
+
path.sep
|
|
726
|
+
)
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
if (
|
|
730
|
+
!isPathInside(
|
|
731
|
+
root,
|
|
732
|
+
candidate
|
|
733
|
+
)
|
|
734
|
+
) {
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
let stat:
|
|
739
|
+
Awaited<
|
|
740
|
+
ReturnType<typeof fs.stat>
|
|
741
|
+
>;
|
|
742
|
+
|
|
743
|
+
try {
|
|
744
|
+
stat =
|
|
745
|
+
await fs.stat(
|
|
746
|
+
candidate
|
|
747
|
+
);
|
|
748
|
+
} catch {
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (!stat.isFile()) {
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const contentType =
|
|
757
|
+
getContentType(
|
|
758
|
+
candidate
|
|
759
|
+
);
|
|
760
|
+
|
|
761
|
+
const encoding =
|
|
762
|
+
selectContentEncoding(
|
|
763
|
+
req,
|
|
764
|
+
contentType,
|
|
765
|
+
stat.size,
|
|
766
|
+
{
|
|
767
|
+
br:
|
|
768
|
+
await fileExists(
|
|
769
|
+
`${candidate}.br`
|
|
770
|
+
),
|
|
771
|
+
gzip:
|
|
772
|
+
await fileExists(
|
|
773
|
+
`${candidate}.gz`
|
|
774
|
+
),
|
|
775
|
+
}
|
|
776
|
+
);
|
|
777
|
+
|
|
778
|
+
const sourceFile =
|
|
779
|
+
encoding === "br"
|
|
780
|
+
? `${candidate}.br`
|
|
781
|
+
: encoding === "gzip"
|
|
782
|
+
? `${candidate}.gz`
|
|
783
|
+
: candidate;
|
|
784
|
+
|
|
785
|
+
const content =
|
|
786
|
+
await fs.readFile(
|
|
787
|
+
sourceFile
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
const headers:
|
|
791
|
+
Record<string, string | number> = {
|
|
792
|
+
"Content-Type":
|
|
793
|
+
contentType,
|
|
794
|
+
"Cache-Control":
|
|
795
|
+
"public, max-age=31536000, immutable",
|
|
796
|
+
"X-Content-Type-Options":
|
|
797
|
+
"nosniff",
|
|
798
|
+
"Content-Length":
|
|
799
|
+
content.length,
|
|
800
|
+
Vary:
|
|
801
|
+
"Accept-Encoding",
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
if (encoding) {
|
|
805
|
+
headers[
|
|
806
|
+
"Content-Encoding"
|
|
807
|
+
] =
|
|
808
|
+
encoding;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
res.writeHead(
|
|
812
|
+
200,
|
|
813
|
+
headers
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
if (
|
|
817
|
+
req.method === "HEAD"
|
|
818
|
+
) {
|
|
819
|
+
res.end();
|
|
820
|
+
return true;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
res.end(
|
|
824
|
+
content
|
|
825
|
+
);
|
|
826
|
+
|
|
827
|
+
return true;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
async function fileExists(
|
|
831
|
+
filePath: string
|
|
832
|
+
): Promise<boolean> {
|
|
833
|
+
try {
|
|
834
|
+
const stat =
|
|
835
|
+
await fs.stat(
|
|
836
|
+
filePath
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
return stat.isFile();
|
|
840
|
+
} catch {
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function sendPublicAsset(
|
|
846
|
+
req: http.IncomingMessage,
|
|
847
|
+
res: http.ServerResponse,
|
|
848
|
+
asset: StaticAsset
|
|
849
|
+
): void {
|
|
850
|
+
const encoding =
|
|
851
|
+
selectContentEncoding(
|
|
852
|
+
req,
|
|
853
|
+
asset.contentType,
|
|
854
|
+
asset.content.length
|
|
855
|
+
);
|
|
856
|
+
|
|
857
|
+
const content =
|
|
858
|
+
compressDynamicContent(
|
|
859
|
+
asset.content,
|
|
860
|
+
encoding
|
|
861
|
+
);
|
|
862
|
+
|
|
863
|
+
const etag =
|
|
864
|
+
createVariantEtag(
|
|
865
|
+
asset.etag,
|
|
866
|
+
encoding
|
|
867
|
+
);
|
|
868
|
+
|
|
869
|
+
const ifNoneMatch =
|
|
870
|
+
req.headers[
|
|
871
|
+
"if-none-match"
|
|
872
|
+
];
|
|
873
|
+
|
|
874
|
+
const values =
|
|
875
|
+
Array.isArray(
|
|
876
|
+
ifNoneMatch
|
|
877
|
+
)
|
|
878
|
+
? ifNoneMatch
|
|
879
|
+
: ifNoneMatch
|
|
880
|
+
? ifNoneMatch.split(",")
|
|
881
|
+
: [];
|
|
882
|
+
|
|
883
|
+
if (
|
|
884
|
+
values
|
|
885
|
+
.map(
|
|
886
|
+
(value) =>
|
|
887
|
+
value.trim()
|
|
888
|
+
)
|
|
889
|
+
.includes(
|
|
890
|
+
etag
|
|
891
|
+
)
|
|
892
|
+
) {
|
|
893
|
+
res.writeHead(
|
|
894
|
+
304,
|
|
895
|
+
{
|
|
896
|
+
ETag:
|
|
897
|
+
etag,
|
|
898
|
+
"Cache-Control":
|
|
899
|
+
"public, max-age=0, must-revalidate",
|
|
900
|
+
Vary:
|
|
901
|
+
"Accept-Encoding",
|
|
902
|
+
}
|
|
903
|
+
);
|
|
904
|
+
res.end();
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const headers:
|
|
909
|
+
Record<string, string | number> = {
|
|
910
|
+
"Content-Type":
|
|
911
|
+
asset.contentType,
|
|
912
|
+
"Content-Length":
|
|
913
|
+
content.length,
|
|
914
|
+
ETag:
|
|
915
|
+
etag,
|
|
916
|
+
"Last-Modified":
|
|
917
|
+
asset.lastModified,
|
|
918
|
+
"Cache-Control":
|
|
919
|
+
"public, max-age=0, must-revalidate",
|
|
920
|
+
"X-Content-Type-Options":
|
|
921
|
+
"nosniff",
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
if (
|
|
925
|
+
isCompressibleContentType(
|
|
926
|
+
asset.contentType
|
|
927
|
+
)
|
|
928
|
+
) {
|
|
929
|
+
headers.Vary =
|
|
930
|
+
"Accept-Encoding";
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (encoding) {
|
|
934
|
+
headers[
|
|
935
|
+
"Content-Encoding"
|
|
936
|
+
] =
|
|
937
|
+
encoding;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
res.writeHead(
|
|
941
|
+
200,
|
|
942
|
+
headers
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
if (
|
|
946
|
+
req.method === "HEAD"
|
|
947
|
+
) {
|
|
948
|
+
res.end();
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
res.end(
|
|
953
|
+
content
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
async function handleApiRoute(
|
|
958
|
+
req: http.IncomingMessage,
|
|
959
|
+
res: http.ServerResponse,
|
|
960
|
+
requestUrl: URL,
|
|
961
|
+
route: ApiRoute,
|
|
962
|
+
params: Record<string, string>,
|
|
963
|
+
buildId: string
|
|
964
|
+
): Promise<void> {
|
|
965
|
+
const moduleUrl =
|
|
966
|
+
createVersionedModuleUrl(
|
|
967
|
+
route.filePath,
|
|
968
|
+
buildId
|
|
969
|
+
);
|
|
970
|
+
|
|
971
|
+
const apiModule =
|
|
972
|
+
await import(
|
|
973
|
+
moduleUrl
|
|
974
|
+
);
|
|
975
|
+
|
|
976
|
+
const method =
|
|
977
|
+
normalizeHttpMethod(
|
|
978
|
+
req.method
|
|
979
|
+
);
|
|
980
|
+
|
|
981
|
+
const allowedMethods =
|
|
982
|
+
getAllowedMethods(
|
|
983
|
+
apiModule
|
|
984
|
+
);
|
|
985
|
+
|
|
986
|
+
if (
|
|
987
|
+
method === "OPTIONS" &&
|
|
988
|
+
!getApiHandler(
|
|
989
|
+
apiModule,
|
|
990
|
+
"OPTIONS"
|
|
991
|
+
)
|
|
992
|
+
) {
|
|
993
|
+
res.writeHead(
|
|
994
|
+
204,
|
|
995
|
+
{
|
|
996
|
+
Allow:
|
|
997
|
+
allowedMethods.join(
|
|
998
|
+
", "
|
|
999
|
+
),
|
|
1000
|
+
}
|
|
1001
|
+
);
|
|
1002
|
+
res.end();
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
let handler =
|
|
1007
|
+
getApiHandler(
|
|
1008
|
+
apiModule,
|
|
1009
|
+
method
|
|
1010
|
+
);
|
|
1011
|
+
|
|
1012
|
+
if (
|
|
1013
|
+
!handler &&
|
|
1014
|
+
method === "HEAD"
|
|
1015
|
+
) {
|
|
1016
|
+
handler =
|
|
1017
|
+
getApiHandler(
|
|
1018
|
+
apiModule,
|
|
1019
|
+
"GET"
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (!handler) {
|
|
1024
|
+
sendMethodNotAllowed(
|
|
1025
|
+
res,
|
|
1026
|
+
allowedMethods
|
|
1027
|
+
);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const response =
|
|
1032
|
+
await handler(
|
|
1033
|
+
await createWebRequest(
|
|
1034
|
+
req,
|
|
1035
|
+
requestUrl
|
|
1036
|
+
),
|
|
1037
|
+
{
|
|
1038
|
+
params,
|
|
1039
|
+
}
|
|
1040
|
+
);
|
|
1041
|
+
|
|
1042
|
+
if (
|
|
1043
|
+
!(response instanceof Response)
|
|
1044
|
+
) {
|
|
1045
|
+
throw new Error(
|
|
1046
|
+
`API handler "${method} ${route.pathname}" must return a Response object.`
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
await sendWebResponse(
|
|
1051
|
+
req,
|
|
1052
|
+
res,
|
|
1053
|
+
response,
|
|
1054
|
+
method === "HEAD"
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function normalizeHttpMethod(
|
|
1059
|
+
method:
|
|
1060
|
+
string | undefined
|
|
1061
|
+
): HttpMethod {
|
|
1062
|
+
return (
|
|
1063
|
+
(
|
|
1064
|
+
method ??
|
|
1065
|
+
"GET"
|
|
1066
|
+
).toUpperCase() as
|
|
1067
|
+
HttpMethod
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function getApiHandler(
|
|
1072
|
+
module:
|
|
1073
|
+
Record<string, unknown>,
|
|
1074
|
+
method: string
|
|
1075
|
+
): ApiHandler | undefined {
|
|
1076
|
+
const value =
|
|
1077
|
+
module[
|
|
1078
|
+
method
|
|
1079
|
+
];
|
|
1080
|
+
|
|
1081
|
+
return typeof value ===
|
|
1082
|
+
"function"
|
|
1083
|
+
? value as ApiHandler
|
|
1084
|
+
: undefined;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function getAllowedMethods(
|
|
1088
|
+
module:
|
|
1089
|
+
Record<string, unknown>
|
|
1090
|
+
): string[] {
|
|
1091
|
+
const allowed:
|
|
1092
|
+
string[] = [];
|
|
1093
|
+
|
|
1094
|
+
for (
|
|
1095
|
+
const method
|
|
1096
|
+
of HTTP_METHODS
|
|
1097
|
+
) {
|
|
1098
|
+
if (
|
|
1099
|
+
method === "OPTIONS"
|
|
1100
|
+
) {
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
if (
|
|
1105
|
+
method === "HEAD"
|
|
1106
|
+
) {
|
|
1107
|
+
if (
|
|
1108
|
+
getApiHandler(
|
|
1109
|
+
module,
|
|
1110
|
+
"HEAD"
|
|
1111
|
+
) ||
|
|
1112
|
+
getApiHandler(
|
|
1113
|
+
module,
|
|
1114
|
+
"GET"
|
|
1115
|
+
)
|
|
1116
|
+
) {
|
|
1117
|
+
allowed.push(
|
|
1118
|
+
"HEAD"
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
if (
|
|
1125
|
+
getApiHandler(
|
|
1126
|
+
module,
|
|
1127
|
+
method
|
|
1128
|
+
)
|
|
1129
|
+
) {
|
|
1130
|
+
allowed.push(
|
|
1131
|
+
method
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
allowed.push(
|
|
1137
|
+
"OPTIONS"
|
|
1138
|
+
);
|
|
1139
|
+
|
|
1140
|
+
return allowed;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
async function createWebRequest(
|
|
1144
|
+
req: http.IncomingMessage,
|
|
1145
|
+
url: URL
|
|
1146
|
+
): Promise<Request> {
|
|
1147
|
+
const headers =
|
|
1148
|
+
new Headers();
|
|
1149
|
+
|
|
1150
|
+
for (
|
|
1151
|
+
const [
|
|
1152
|
+
key,
|
|
1153
|
+
value,
|
|
1154
|
+
]
|
|
1155
|
+
of Object.entries(
|
|
1156
|
+
req.headers
|
|
1157
|
+
)
|
|
1158
|
+
) {
|
|
1159
|
+
if (
|
|
1160
|
+
value ===
|
|
1161
|
+
undefined
|
|
1162
|
+
) {
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
if (
|
|
1167
|
+
Array.isArray(
|
|
1168
|
+
value
|
|
1169
|
+
)
|
|
1170
|
+
) {
|
|
1171
|
+
for (
|
|
1172
|
+
const item
|
|
1173
|
+
of value
|
|
1174
|
+
) {
|
|
1175
|
+
headers.append(
|
|
1176
|
+
key,
|
|
1177
|
+
item
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
headers.set(
|
|
1184
|
+
key,
|
|
1185
|
+
value
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
const method =
|
|
1190
|
+
(
|
|
1191
|
+
req.method ??
|
|
1192
|
+
"GET"
|
|
1193
|
+
).toUpperCase();
|
|
1194
|
+
|
|
1195
|
+
const init:
|
|
1196
|
+
RequestInit = {
|
|
1197
|
+
method,
|
|
1198
|
+
headers,
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
if (
|
|
1202
|
+
method !== "GET" &&
|
|
1203
|
+
method !== "HEAD"
|
|
1204
|
+
) {
|
|
1205
|
+
const body =
|
|
1206
|
+
await readRequestBody(
|
|
1207
|
+
req
|
|
1208
|
+
);
|
|
1209
|
+
|
|
1210
|
+
if (
|
|
1211
|
+
body.length > 0
|
|
1212
|
+
) {
|
|
1213
|
+
init.body =
|
|
1214
|
+
body as unknown as
|
|
1215
|
+
BodyInit;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
return new Request(
|
|
1220
|
+
url,
|
|
1221
|
+
init
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
async function readRequestBody(
|
|
1226
|
+
req: http.IncomingMessage
|
|
1227
|
+
): Promise<Buffer> {
|
|
1228
|
+
const chunks:
|
|
1229
|
+
Buffer[] = [];
|
|
1230
|
+
|
|
1231
|
+
for await (
|
|
1232
|
+
const chunk
|
|
1233
|
+
of req
|
|
1234
|
+
) {
|
|
1235
|
+
chunks.push(
|
|
1236
|
+
Buffer.isBuffer(
|
|
1237
|
+
chunk
|
|
1238
|
+
)
|
|
1239
|
+
? chunk
|
|
1240
|
+
: Buffer.from(
|
|
1241
|
+
chunk
|
|
1242
|
+
)
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
return Buffer.concat(
|
|
1247
|
+
chunks
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
async function sendWebResponse(
|
|
1252
|
+
req: http.IncomingMessage,
|
|
1253
|
+
res: http.ServerResponse,
|
|
1254
|
+
response: Response,
|
|
1255
|
+
isHead: boolean
|
|
1256
|
+
): Promise<void> {
|
|
1257
|
+
res.statusCode =
|
|
1258
|
+
response.status;
|
|
1259
|
+
|
|
1260
|
+
response.headers.forEach(
|
|
1261
|
+
(
|
|
1262
|
+
value,
|
|
1263
|
+
key
|
|
1264
|
+
) => {
|
|
1265
|
+
if (
|
|
1266
|
+
key.toLowerCase() ===
|
|
1267
|
+
"set-cookie" ||
|
|
1268
|
+
key.toLowerCase() ===
|
|
1269
|
+
"content-length"
|
|
1270
|
+
) {
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
res.setHeader(
|
|
1275
|
+
key,
|
|
1276
|
+
value
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
);
|
|
1280
|
+
|
|
1281
|
+
const responseHeaders =
|
|
1282
|
+
response.headers as
|
|
1283
|
+
Headers & {
|
|
1284
|
+
getSetCookie?: () =>
|
|
1285
|
+
string[];
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
const cookies =
|
|
1289
|
+
responseHeaders
|
|
1290
|
+
.getSetCookie?.();
|
|
1291
|
+
|
|
1292
|
+
if (
|
|
1293
|
+
cookies &&
|
|
1294
|
+
cookies.length > 0
|
|
1295
|
+
) {
|
|
1296
|
+
res.setHeader(
|
|
1297
|
+
"Set-Cookie",
|
|
1298
|
+
cookies
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
if (
|
|
1303
|
+
response.status === 204 ||
|
|
1304
|
+
response.status === 304
|
|
1305
|
+
) {
|
|
1306
|
+
res.end();
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
const rawBody =
|
|
1311
|
+
Buffer.from(
|
|
1312
|
+
await response.arrayBuffer()
|
|
1313
|
+
);
|
|
1314
|
+
|
|
1315
|
+
const contentType =
|
|
1316
|
+
response.headers.get(
|
|
1317
|
+
"content-type"
|
|
1318
|
+
) ??
|
|
1319
|
+
"application/octet-stream";
|
|
1320
|
+
|
|
1321
|
+
const existingEncoding =
|
|
1322
|
+
response.headers.get(
|
|
1323
|
+
"content-encoding"
|
|
1324
|
+
);
|
|
1325
|
+
|
|
1326
|
+
const encoding =
|
|
1327
|
+
existingEncoding
|
|
1328
|
+
? null
|
|
1329
|
+
: selectContentEncoding(
|
|
1330
|
+
req,
|
|
1331
|
+
contentType,
|
|
1332
|
+
rawBody.length
|
|
1333
|
+
);
|
|
1334
|
+
|
|
1335
|
+
const body =
|
|
1336
|
+
existingEncoding
|
|
1337
|
+
? rawBody
|
|
1338
|
+
: compressDynamicContent(
|
|
1339
|
+
rawBody,
|
|
1340
|
+
encoding
|
|
1341
|
+
);
|
|
1342
|
+
|
|
1343
|
+
if (
|
|
1344
|
+
encoding
|
|
1345
|
+
) {
|
|
1346
|
+
res.setHeader(
|
|
1347
|
+
"Content-Encoding",
|
|
1348
|
+
encoding
|
|
1349
|
+
);
|
|
1350
|
+
res.setHeader(
|
|
1351
|
+
"Vary",
|
|
1352
|
+
"Accept-Encoding"
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
res.setHeader(
|
|
1357
|
+
"Content-Length",
|
|
1358
|
+
body.length
|
|
1359
|
+
);
|
|
1360
|
+
|
|
1361
|
+
if (isHead) {
|
|
1362
|
+
res.end();
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
res.end(
|
|
1367
|
+
body
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
async function importComponent(
|
|
1372
|
+
filePath: string,
|
|
1373
|
+
componentType: string,
|
|
1374
|
+
buildId: string
|
|
1375
|
+
): Promise<ComponentType<any>> {
|
|
1376
|
+
const module =
|
|
1377
|
+
await import(
|
|
1378
|
+
createVersionedModuleUrl(
|
|
1379
|
+
filePath,
|
|
1380
|
+
buildId
|
|
1381
|
+
)
|
|
1382
|
+
);
|
|
1383
|
+
|
|
1384
|
+
const Component =
|
|
1385
|
+
module.default;
|
|
1386
|
+
|
|
1387
|
+
if (
|
|
1388
|
+
typeof Component !==
|
|
1389
|
+
"function"
|
|
1390
|
+
) {
|
|
1391
|
+
throw new Error(
|
|
1392
|
+
`${componentType} "${filePath}" does not export a default React component.`
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
return Component;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function createVersionedModuleUrl(
|
|
1400
|
+
filePath: string,
|
|
1401
|
+
buildId: string
|
|
1402
|
+
): string {
|
|
1403
|
+
const url =
|
|
1404
|
+
pathToFileURL(
|
|
1405
|
+
filePath
|
|
1406
|
+
);
|
|
1407
|
+
|
|
1408
|
+
url.searchParams.set(
|
|
1409
|
+
"bcp-build",
|
|
1410
|
+
buildId
|
|
1411
|
+
);
|
|
1412
|
+
|
|
1413
|
+
return url.href;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function renderDocument(
|
|
1417
|
+
content: string,
|
|
1418
|
+
routeAsset: ProductionRouteAsset,
|
|
1419
|
+
frameworkData: FrameworkData
|
|
1420
|
+
): string {
|
|
1421
|
+
const modulePreloads =
|
|
1422
|
+
routeAsset.imports
|
|
1423
|
+
.map(
|
|
1424
|
+
(importPath) =>
|
|
1425
|
+
` <link rel="modulepreload" href="${escapeHtmlAttribute(
|
|
1426
|
+
importPath
|
|
1427
|
+
)}" />`
|
|
1428
|
+
)
|
|
1429
|
+
.join("\n");
|
|
1430
|
+
|
|
1431
|
+
return `
|
|
1432
|
+
<!DOCTYPE html>
|
|
1433
|
+
<html lang="en">
|
|
1434
|
+
<head>
|
|
1435
|
+
<meta charset="UTF-8" />
|
|
1436
|
+
<meta
|
|
1437
|
+
name="viewport"
|
|
1438
|
+
content="width=device-width, initial-scale=1.0"
|
|
1439
|
+
/>
|
|
1440
|
+
<title>BCP Framework</title>
|
|
1441
|
+
${modulePreloads}
|
|
1442
|
+
</head>
|
|
1443
|
+
<body>
|
|
1444
|
+
<div id="root">${content}</div>
|
|
1445
|
+
|
|
1446
|
+
<script
|
|
1447
|
+
id="__BCP_DATA__"
|
|
1448
|
+
type="application/json"
|
|
1449
|
+
>${serializeFrameworkData(frameworkData)}</script>
|
|
1450
|
+
|
|
1451
|
+
<script
|
|
1452
|
+
data-bcp-client
|
|
1453
|
+
type="module"
|
|
1454
|
+
src="${escapeHtmlAttribute(
|
|
1455
|
+
routeAsset.entry
|
|
1456
|
+
)}"
|
|
1457
|
+
></script>
|
|
1458
|
+
</body>
|
|
1459
|
+
</html>
|
|
1460
|
+
`.trim();
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
function serializeFrameworkData(
|
|
1464
|
+
value: unknown
|
|
1465
|
+
): string {
|
|
1466
|
+
return JSON.stringify(
|
|
1467
|
+
value
|
|
1468
|
+
)
|
|
1469
|
+
.replace(
|
|
1470
|
+
/</g,
|
|
1471
|
+
"\\u003c"
|
|
1472
|
+
)
|
|
1473
|
+
.replace(
|
|
1474
|
+
/\u2028/g,
|
|
1475
|
+
"\\u2028"
|
|
1476
|
+
)
|
|
1477
|
+
.replace(
|
|
1478
|
+
/\u2029/g,
|
|
1479
|
+
"\\u2029"
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
function sendHtml(
|
|
1484
|
+
req: http.IncomingMessage,
|
|
1485
|
+
res: http.ServerResponse,
|
|
1486
|
+
html: string
|
|
1487
|
+
): void {
|
|
1488
|
+
const contentType =
|
|
1489
|
+
"text/html; charset=utf-8";
|
|
1490
|
+
|
|
1491
|
+
const rawBody =
|
|
1492
|
+
Buffer.from(
|
|
1493
|
+
html
|
|
1494
|
+
);
|
|
1495
|
+
|
|
1496
|
+
const encoding =
|
|
1497
|
+
selectContentEncoding(
|
|
1498
|
+
req,
|
|
1499
|
+
contentType,
|
|
1500
|
+
rawBody.length
|
|
1501
|
+
);
|
|
1502
|
+
|
|
1503
|
+
const body =
|
|
1504
|
+
compressDynamicContent(
|
|
1505
|
+
rawBody,
|
|
1506
|
+
encoding
|
|
1507
|
+
);
|
|
1508
|
+
|
|
1509
|
+
const headers:
|
|
1510
|
+
Record<string, string | number> = {
|
|
1511
|
+
"Content-Type":
|
|
1512
|
+
contentType,
|
|
1513
|
+
"Cache-Control":
|
|
1514
|
+
"no-store",
|
|
1515
|
+
"Content-Length":
|
|
1516
|
+
body.length,
|
|
1517
|
+
Vary:
|
|
1518
|
+
"Accept-Encoding",
|
|
1519
|
+
};
|
|
1520
|
+
|
|
1521
|
+
if (encoding) {
|
|
1522
|
+
headers[
|
|
1523
|
+
"Content-Encoding"
|
|
1524
|
+
] =
|
|
1525
|
+
encoding;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
res.writeHead(
|
|
1529
|
+
200,
|
|
1530
|
+
headers
|
|
1531
|
+
);
|
|
1532
|
+
|
|
1533
|
+
if (
|
|
1534
|
+
req.method === "HEAD"
|
|
1535
|
+
) {
|
|
1536
|
+
res.end();
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
res.end(
|
|
1541
|
+
body
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
function sendJavaScript(
|
|
1546
|
+
res: http.ServerResponse,
|
|
1547
|
+
content: Buffer,
|
|
1548
|
+
immutable = true
|
|
1549
|
+
): void {
|
|
1550
|
+
res.writeHead(
|
|
1551
|
+
200,
|
|
1552
|
+
{
|
|
1553
|
+
"Content-Type":
|
|
1554
|
+
"text/javascript; charset=utf-8",
|
|
1555
|
+
"Cache-Control":
|
|
1556
|
+
immutable
|
|
1557
|
+
? "public, max-age=31536000, immutable"
|
|
1558
|
+
: "no-store",
|
|
1559
|
+
"Content-Length":
|
|
1560
|
+
content.length,
|
|
1561
|
+
}
|
|
1562
|
+
);
|
|
1563
|
+
|
|
1564
|
+
res.end(
|
|
1565
|
+
content
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
function sendMethodNotAllowed(
|
|
1570
|
+
res: http.ServerResponse,
|
|
1571
|
+
methods: string[]
|
|
1572
|
+
): void {
|
|
1573
|
+
const body =
|
|
1574
|
+
JSON.stringify({
|
|
1575
|
+
error:
|
|
1576
|
+
"Method Not Allowed",
|
|
1577
|
+
allowedMethods:
|
|
1578
|
+
methods,
|
|
1579
|
+
});
|
|
1580
|
+
|
|
1581
|
+
res.writeHead(
|
|
1582
|
+
405,
|
|
1583
|
+
{
|
|
1584
|
+
"Content-Type":
|
|
1585
|
+
"application/json; charset=utf-8",
|
|
1586
|
+
Allow:
|
|
1587
|
+
methods.join(
|
|
1588
|
+
", "
|
|
1589
|
+
),
|
|
1590
|
+
"Content-Length":
|
|
1591
|
+
Buffer.byteLength(
|
|
1592
|
+
body
|
|
1593
|
+
),
|
|
1594
|
+
}
|
|
1595
|
+
);
|
|
1596
|
+
|
|
1597
|
+
res.end(
|
|
1598
|
+
body
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
function sendNotFound(
|
|
1603
|
+
res: http.ServerResponse
|
|
1604
|
+
): void {
|
|
1605
|
+
const body =
|
|
1606
|
+
"<!DOCTYPE html><html><body><h1>404</h1><p>Page Not Found</p></body></html>";
|
|
1607
|
+
|
|
1608
|
+
res.writeHead(
|
|
1609
|
+
404,
|
|
1610
|
+
{
|
|
1611
|
+
"Content-Type":
|
|
1612
|
+
"text/html; charset=utf-8",
|
|
1613
|
+
"Content-Length":
|
|
1614
|
+
Buffer.byteLength(
|
|
1615
|
+
body
|
|
1616
|
+
),
|
|
1617
|
+
}
|
|
1618
|
+
);
|
|
1619
|
+
|
|
1620
|
+
res.end(
|
|
1621
|
+
body
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
function sendInternalServerError(
|
|
1626
|
+
res: http.ServerResponse,
|
|
1627
|
+
error: unknown
|
|
1628
|
+
): void {
|
|
1629
|
+
const message =
|
|
1630
|
+
error instanceof Error
|
|
1631
|
+
? error.message
|
|
1632
|
+
: "Unknown error";
|
|
1633
|
+
|
|
1634
|
+
const body =
|
|
1635
|
+
`<!DOCTYPE html><html><body><h1>500</h1><p>Internal Server Error</p><pre>${escapeHtml(
|
|
1636
|
+
message
|
|
1637
|
+
)}</pre></body></html>`;
|
|
1638
|
+
|
|
1639
|
+
res.writeHead(
|
|
1640
|
+
500,
|
|
1641
|
+
{
|
|
1642
|
+
"Content-Type":
|
|
1643
|
+
"text/html; charset=utf-8",
|
|
1644
|
+
"Cache-Control":
|
|
1645
|
+
"no-store",
|
|
1646
|
+
"Content-Length":
|
|
1647
|
+
Buffer.byteLength(
|
|
1648
|
+
body
|
|
1649
|
+
),
|
|
1650
|
+
}
|
|
1651
|
+
);
|
|
1652
|
+
|
|
1653
|
+
res.end(
|
|
1654
|
+
body
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
function createVariantEtag(
|
|
1659
|
+
etag: string,
|
|
1660
|
+
encoding: ContentEncoding
|
|
1661
|
+
): string {
|
|
1662
|
+
if (!encoding) {
|
|
1663
|
+
return etag;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
const value =
|
|
1667
|
+
etag
|
|
1668
|
+
.replace(/^W\//, "")
|
|
1669
|
+
.replace(/^"|"$/g, "");
|
|
1670
|
+
|
|
1671
|
+
return `W/"${value}-${encoding}"`;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
function normalizePathname(
|
|
1675
|
+
pathname: string
|
|
1676
|
+
): string {
|
|
1677
|
+
if (
|
|
1678
|
+
pathname.length > 1 &&
|
|
1679
|
+
pathname.endsWith("/")
|
|
1680
|
+
) {
|
|
1681
|
+
return pathname.slice(
|
|
1682
|
+
0,
|
|
1683
|
+
-1
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
return pathname || "/";
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
function isPathInside(
|
|
1691
|
+
root: string,
|
|
1692
|
+
candidate: string
|
|
1693
|
+
): boolean {
|
|
1694
|
+
const relative =
|
|
1695
|
+
path.relative(
|
|
1696
|
+
root,
|
|
1697
|
+
candidate
|
|
1698
|
+
);
|
|
1699
|
+
|
|
1700
|
+
return (
|
|
1701
|
+
relative === "" ||
|
|
1702
|
+
(
|
|
1703
|
+
relative !== ".." &&
|
|
1704
|
+
!relative.startsWith(
|
|
1705
|
+
`..${path.sep}`
|
|
1706
|
+
) &&
|
|
1707
|
+
!path.isAbsolute(
|
|
1708
|
+
relative
|
|
1709
|
+
)
|
|
1710
|
+
)
|
|
1711
|
+
);
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
function escapeHtml(
|
|
1715
|
+
value: string
|
|
1716
|
+
): string {
|
|
1717
|
+
return value
|
|
1718
|
+
.replaceAll(
|
|
1719
|
+
"&",
|
|
1720
|
+
"&"
|
|
1721
|
+
)
|
|
1722
|
+
.replaceAll(
|
|
1723
|
+
"<",
|
|
1724
|
+
"<"
|
|
1725
|
+
)
|
|
1726
|
+
.replaceAll(
|
|
1727
|
+
">",
|
|
1728
|
+
">"
|
|
1729
|
+
)
|
|
1730
|
+
.replaceAll(
|
|
1731
|
+
'"',
|
|
1732
|
+
"""
|
|
1733
|
+
)
|
|
1734
|
+
.replaceAll(
|
|
1735
|
+
"'",
|
|
1736
|
+
"'"
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
function escapeHtmlAttribute(
|
|
1741
|
+
value: string
|
|
1742
|
+
): string {
|
|
1743
|
+
return escapeHtml(
|
|
1744
|
+
value
|
|
1745
|
+
);
|
|
1746
|
+
}
|