@chidchanun/bcp 0.1.5 → 0.1.7
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 +47 -1
- package/README.md +83 -1
- package/docs/server-request-apis.md +404 -0
- package/package.json +6 -1
- package/packages/bundler/src/client-boundary.ts +8 -6
- package/packages/bundler/src/server-production.ts +17 -0
- package/packages/client/src/server.ts +23 -0
- package/packages/server/src/critical-css-proxy.ts +559 -0
- package/packages/server/src/critical-css.ts +132 -0
- package/packages/server/src/index.ts +32 -13
- package/packages/server/src/middleware-dev-server.ts +59 -3
- package/packages/server/src/request-context.ts +1043 -0
- package/packages/server/src/server-response.ts +82 -0
- package/packages/server/src/standalone-production-runtime-v2.ts +44 -13
- package/packages/server/src/standalone-production-runtime-v6.ts +205 -0
- package/packages/server/src/standalone-production-server.ts +1 -1
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
import * as http from "node:http";
|
|
2
|
+
import {
|
|
3
|
+
brotliCompressSync,
|
|
4
|
+
brotliDecompressSync,
|
|
5
|
+
deflateSync,
|
|
6
|
+
gunzipSync,
|
|
7
|
+
gzipSync,
|
|
8
|
+
inflateSync,
|
|
9
|
+
} from "node:zlib";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
canInlineStylesheet,
|
|
13
|
+
inlineSmallStylesheet,
|
|
14
|
+
} from "./critical-css.js";
|
|
15
|
+
|
|
16
|
+
export interface CriticalCssProxyOptions {
|
|
17
|
+
port: number;
|
|
18
|
+
hostname: string;
|
|
19
|
+
upstreamPort: number;
|
|
20
|
+
upstreamHostname: string;
|
|
21
|
+
cssFile: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type SupportedContentEncoding =
|
|
25
|
+
| "identity"
|
|
26
|
+
| "gzip"
|
|
27
|
+
| "br"
|
|
28
|
+
| "deflate";
|
|
29
|
+
|
|
30
|
+
export function createCriticalCssProxy(
|
|
31
|
+
options: CriticalCssProxyOptions
|
|
32
|
+
) {
|
|
33
|
+
const server =
|
|
34
|
+
http.createServer(
|
|
35
|
+
(
|
|
36
|
+
req,
|
|
37
|
+
res
|
|
38
|
+
) => {
|
|
39
|
+
const upstream =
|
|
40
|
+
http.request({
|
|
41
|
+
hostname:
|
|
42
|
+
options.upstreamHostname,
|
|
43
|
+
port:
|
|
44
|
+
options.upstreamPort,
|
|
45
|
+
method:
|
|
46
|
+
req.method ??
|
|
47
|
+
"GET",
|
|
48
|
+
path:
|
|
49
|
+
req.url ??
|
|
50
|
+
"/",
|
|
51
|
+
headers: {
|
|
52
|
+
...req.headers,
|
|
53
|
+
host:
|
|
54
|
+
req.headers.host ??
|
|
55
|
+
`${options.hostname}:${options.port}`,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
upstream.on(
|
|
60
|
+
"response",
|
|
61
|
+
(upstreamResponse) => {
|
|
62
|
+
const encoding =
|
|
63
|
+
getSupportedContentEncoding(
|
|
64
|
+
upstreamResponse
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
!canInlineStylesheet(
|
|
69
|
+
options.cssFile
|
|
70
|
+
) ||
|
|
71
|
+
!encoding ||
|
|
72
|
+
!shouldTransformHtml(
|
|
73
|
+
req,
|
|
74
|
+
upstreamResponse
|
|
75
|
+
)
|
|
76
|
+
) {
|
|
77
|
+
writeUpstreamHead(
|
|
78
|
+
res,
|
|
79
|
+
upstreamResponse
|
|
80
|
+
);
|
|
81
|
+
upstreamResponse.pipe(
|
|
82
|
+
res
|
|
83
|
+
);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
void transformHtmlResponse(
|
|
88
|
+
res,
|
|
89
|
+
upstreamResponse,
|
|
90
|
+
options.cssFile,
|
|
91
|
+
encoding
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
upstream.on(
|
|
97
|
+
"error",
|
|
98
|
+
(error) => {
|
|
99
|
+
if (
|
|
100
|
+
res.headersSent
|
|
101
|
+
) {
|
|
102
|
+
res.destroy(
|
|
103
|
+
error
|
|
104
|
+
);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
res.writeHead(
|
|
109
|
+
502,
|
|
110
|
+
{
|
|
111
|
+
"Content-Type":
|
|
112
|
+
"text/plain; charset=utf-8",
|
|
113
|
+
"Cache-Control":
|
|
114
|
+
"no-store",
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
res.end(
|
|
118
|
+
"Bad Gateway"
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
req.pipe(
|
|
124
|
+
upstream
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
server.keepAliveTimeout =
|
|
130
|
+
65_000;
|
|
131
|
+
server.headersTimeout =
|
|
132
|
+
66_000;
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
async start(): Promise<void> {
|
|
136
|
+
await listen(
|
|
137
|
+
server,
|
|
138
|
+
options.port,
|
|
139
|
+
options.hostname
|
|
140
|
+
);
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
async stop(): Promise<void> {
|
|
144
|
+
if (
|
|
145
|
+
!server.listening
|
|
146
|
+
) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await new Promise<void>(
|
|
151
|
+
(
|
|
152
|
+
resolve,
|
|
153
|
+
reject
|
|
154
|
+
) => {
|
|
155
|
+
server.close(
|
|
156
|
+
(error) => {
|
|
157
|
+
if (error) {
|
|
158
|
+
reject(
|
|
159
|
+
error
|
|
160
|
+
);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
resolve();
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function shouldTransformHtml(
|
|
174
|
+
req: http.IncomingMessage,
|
|
175
|
+
response: http.IncomingMessage
|
|
176
|
+
): boolean {
|
|
177
|
+
if (
|
|
178
|
+
(req.method ?? "GET").toUpperCase() !==
|
|
179
|
+
"GET"
|
|
180
|
+
) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (
|
|
185
|
+
response.statusCode !== 200
|
|
186
|
+
) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const contentType =
|
|
191
|
+
response.headers[
|
|
192
|
+
"content-type"
|
|
193
|
+
];
|
|
194
|
+
const value =
|
|
195
|
+
Array.isArray(
|
|
196
|
+
contentType
|
|
197
|
+
)
|
|
198
|
+
? contentType[0]
|
|
199
|
+
: contentType;
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
typeof value === "string" &&
|
|
203
|
+
/^text\/html\b/i.test(
|
|
204
|
+
value
|
|
205
|
+
) &&
|
|
206
|
+
allowsInlineStyles(
|
|
207
|
+
response
|
|
208
|
+
)
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function allowsInlineStyles(
|
|
213
|
+
response: http.IncomingMessage
|
|
214
|
+
): boolean {
|
|
215
|
+
const header =
|
|
216
|
+
response.headers[
|
|
217
|
+
"content-security-policy"
|
|
218
|
+
];
|
|
219
|
+
const policies =
|
|
220
|
+
Array.isArray(
|
|
221
|
+
header
|
|
222
|
+
)
|
|
223
|
+
? header
|
|
224
|
+
: header
|
|
225
|
+
? [header]
|
|
226
|
+
: [];
|
|
227
|
+
|
|
228
|
+
if (
|
|
229
|
+
policies.length === 0
|
|
230
|
+
) {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return policies.every(
|
|
235
|
+
(policy) =>
|
|
236
|
+
policyAllowsInlineStyles(
|
|
237
|
+
policy
|
|
238
|
+
)
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function policyAllowsInlineStyles(
|
|
243
|
+
policy: string
|
|
244
|
+
): boolean {
|
|
245
|
+
const directives =
|
|
246
|
+
new Map<string, string[]>();
|
|
247
|
+
|
|
248
|
+
for (
|
|
249
|
+
const rawDirective
|
|
250
|
+
of policy.split(";")
|
|
251
|
+
) {
|
|
252
|
+
const parts =
|
|
253
|
+
rawDirective
|
|
254
|
+
.trim()
|
|
255
|
+
.split(/\s+/)
|
|
256
|
+
.filter(Boolean);
|
|
257
|
+
|
|
258
|
+
if (
|
|
259
|
+
parts.length === 0
|
|
260
|
+
) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
directives.set(
|
|
265
|
+
parts[0].toLowerCase(),
|
|
266
|
+
parts.slice(1)
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
for (
|
|
271
|
+
const name
|
|
272
|
+
of [
|
|
273
|
+
"style-src-elem",
|
|
274
|
+
"style-src",
|
|
275
|
+
"default-src",
|
|
276
|
+
]
|
|
277
|
+
) {
|
|
278
|
+
const sources =
|
|
279
|
+
directives.get(
|
|
280
|
+
name
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
if (!sources) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return sources.some(
|
|
288
|
+
(source) =>
|
|
289
|
+
source.toLowerCase() ===
|
|
290
|
+
"'unsafe-inline'"
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function transformHtmlResponse(
|
|
298
|
+
res: http.ServerResponse,
|
|
299
|
+
upstreamResponse: http.IncomingMessage,
|
|
300
|
+
cssFile: string,
|
|
301
|
+
encoding: SupportedContentEncoding
|
|
302
|
+
): Promise<void> {
|
|
303
|
+
try {
|
|
304
|
+
const chunks:
|
|
305
|
+
Buffer[] = [];
|
|
306
|
+
|
|
307
|
+
for await (
|
|
308
|
+
const chunk
|
|
309
|
+
of upstreamResponse
|
|
310
|
+
) {
|
|
311
|
+
chunks.push(
|
|
312
|
+
Buffer.isBuffer(
|
|
313
|
+
chunk
|
|
314
|
+
)
|
|
315
|
+
? chunk
|
|
316
|
+
: Buffer.from(
|
|
317
|
+
chunk
|
|
318
|
+
)
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const decodedBody =
|
|
323
|
+
decodeBody(
|
|
324
|
+
Buffer.concat(
|
|
325
|
+
chunks
|
|
326
|
+
),
|
|
327
|
+
encoding
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const html =
|
|
331
|
+
inlineSmallStylesheet(
|
|
332
|
+
decodedBody.toString(
|
|
333
|
+
"utf8"
|
|
334
|
+
),
|
|
335
|
+
cssFile
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const responseBody =
|
|
339
|
+
encodeBody(
|
|
340
|
+
Buffer.from(
|
|
341
|
+
html,
|
|
342
|
+
"utf8"
|
|
343
|
+
),
|
|
344
|
+
encoding
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const headers:
|
|
348
|
+
http.OutgoingHttpHeaders = {
|
|
349
|
+
...upstreamResponse.headers,
|
|
350
|
+
"content-length":
|
|
351
|
+
responseBody.length,
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
delete headers[
|
|
355
|
+
"transfer-encoding"
|
|
356
|
+
];
|
|
357
|
+
delete headers[
|
|
358
|
+
"etag"
|
|
359
|
+
];
|
|
360
|
+
delete headers[
|
|
361
|
+
"content-md5"
|
|
362
|
+
];
|
|
363
|
+
|
|
364
|
+
if (
|
|
365
|
+
encoding === "identity"
|
|
366
|
+
) {
|
|
367
|
+
delete headers[
|
|
368
|
+
"content-encoding"
|
|
369
|
+
];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
writeUpstreamHead(
|
|
373
|
+
res,
|
|
374
|
+
upstreamResponse,
|
|
375
|
+
headers
|
|
376
|
+
);
|
|
377
|
+
res.end(
|
|
378
|
+
responseBody
|
|
379
|
+
);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
if (
|
|
382
|
+
res.headersSent
|
|
383
|
+
) {
|
|
384
|
+
res.destroy(
|
|
385
|
+
error instanceof Error
|
|
386
|
+
? error
|
|
387
|
+
: undefined
|
|
388
|
+
);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
res.writeHead(
|
|
393
|
+
500,
|
|
394
|
+
{
|
|
395
|
+
"Content-Type":
|
|
396
|
+
"text/plain; charset=utf-8",
|
|
397
|
+
"Cache-Control":
|
|
398
|
+
"no-store",
|
|
399
|
+
}
|
|
400
|
+
);
|
|
401
|
+
res.end(
|
|
402
|
+
"Internal Server Error"
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function getSupportedContentEncoding(
|
|
408
|
+
response: http.IncomingMessage
|
|
409
|
+
): SupportedContentEncoding | null {
|
|
410
|
+
const header =
|
|
411
|
+
response.headers[
|
|
412
|
+
"content-encoding"
|
|
413
|
+
];
|
|
414
|
+
const value =
|
|
415
|
+
(
|
|
416
|
+
Array.isArray(
|
|
417
|
+
header
|
|
418
|
+
)
|
|
419
|
+
? header[0]
|
|
420
|
+
: header
|
|
421
|
+
)
|
|
422
|
+
?.trim()
|
|
423
|
+
.toLowerCase();
|
|
424
|
+
|
|
425
|
+
if (
|
|
426
|
+
!value ||
|
|
427
|
+
value === "identity"
|
|
428
|
+
) {
|
|
429
|
+
return "identity";
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (
|
|
433
|
+
value === "gzip" ||
|
|
434
|
+
value === "br" ||
|
|
435
|
+
value === "deflate"
|
|
436
|
+
) {
|
|
437
|
+
return value;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function decodeBody(
|
|
444
|
+
body: Buffer,
|
|
445
|
+
encoding: SupportedContentEncoding
|
|
446
|
+
): Buffer {
|
|
447
|
+
switch (encoding) {
|
|
448
|
+
case "gzip":
|
|
449
|
+
return gunzipSync(
|
|
450
|
+
body
|
|
451
|
+
);
|
|
452
|
+
case "br":
|
|
453
|
+
return brotliDecompressSync(
|
|
454
|
+
body
|
|
455
|
+
);
|
|
456
|
+
case "deflate":
|
|
457
|
+
return inflateSync(
|
|
458
|
+
body
|
|
459
|
+
);
|
|
460
|
+
default:
|
|
461
|
+
return body;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function encodeBody(
|
|
466
|
+
body: Buffer,
|
|
467
|
+
encoding: SupportedContentEncoding
|
|
468
|
+
): Buffer {
|
|
469
|
+
switch (encoding) {
|
|
470
|
+
case "gzip":
|
|
471
|
+
return gzipSync(
|
|
472
|
+
body
|
|
473
|
+
);
|
|
474
|
+
case "br":
|
|
475
|
+
return brotliCompressSync(
|
|
476
|
+
body
|
|
477
|
+
);
|
|
478
|
+
case "deflate":
|
|
479
|
+
return deflateSync(
|
|
480
|
+
body
|
|
481
|
+
);
|
|
482
|
+
default:
|
|
483
|
+
return body;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function writeUpstreamHead(
|
|
488
|
+
res: http.ServerResponse,
|
|
489
|
+
upstreamResponse: http.IncomingMessage,
|
|
490
|
+
headers:
|
|
491
|
+
http.OutgoingHttpHeaders =
|
|
492
|
+
upstreamResponse.headers
|
|
493
|
+
): void {
|
|
494
|
+
const statusCode =
|
|
495
|
+
upstreamResponse.statusCode ??
|
|
496
|
+
502;
|
|
497
|
+
|
|
498
|
+
if (
|
|
499
|
+
upstreamResponse.statusMessage
|
|
500
|
+
) {
|
|
501
|
+
res.writeHead(
|
|
502
|
+
statusCode,
|
|
503
|
+
upstreamResponse.statusMessage,
|
|
504
|
+
headers
|
|
505
|
+
);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
res.writeHead(
|
|
510
|
+
statusCode,
|
|
511
|
+
headers
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async function listen(
|
|
516
|
+
server: http.Server,
|
|
517
|
+
port: number,
|
|
518
|
+
hostname: string
|
|
519
|
+
): Promise<void> {
|
|
520
|
+
await new Promise<void>(
|
|
521
|
+
(
|
|
522
|
+
resolve,
|
|
523
|
+
reject
|
|
524
|
+
) => {
|
|
525
|
+
const onError =
|
|
526
|
+
(error: Error) => {
|
|
527
|
+
server.off(
|
|
528
|
+
"listening",
|
|
529
|
+
onListening
|
|
530
|
+
);
|
|
531
|
+
reject(
|
|
532
|
+
error
|
|
533
|
+
);
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const onListening =
|
|
537
|
+
() => {
|
|
538
|
+
server.off(
|
|
539
|
+
"error",
|
|
540
|
+
onError
|
|
541
|
+
);
|
|
542
|
+
resolve();
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
server.once(
|
|
546
|
+
"error",
|
|
547
|
+
onError
|
|
548
|
+
);
|
|
549
|
+
server.once(
|
|
550
|
+
"listening",
|
|
551
|
+
onListening
|
|
552
|
+
);
|
|
553
|
+
server.listen(
|
|
554
|
+
port,
|
|
555
|
+
hostname
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
);
|
|
559
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_INLINE_STYLESHEET_LIMIT =
|
|
5
|
+
8 * 1024;
|
|
6
|
+
|
|
7
|
+
export interface InlineStylesheetOptions {
|
|
8
|
+
href?: string;
|
|
9
|
+
maxBytes?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function canInlineStylesheet(
|
|
13
|
+
cssFile: string,
|
|
14
|
+
maxBytes =
|
|
15
|
+
DEFAULT_INLINE_STYLESHEET_LIMIT
|
|
16
|
+
): boolean {
|
|
17
|
+
assertValidMaxBytes(
|
|
18
|
+
maxBytes
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const stat =
|
|
23
|
+
fs.statSync(
|
|
24
|
+
cssFile
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
stat.isFile() &&
|
|
29
|
+
stat.size <= maxBytes
|
|
30
|
+
);
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function inlineSmallStylesheet(
|
|
37
|
+
html: string,
|
|
38
|
+
cssFile: string,
|
|
39
|
+
options: InlineStylesheetOptions = {}
|
|
40
|
+
): string {
|
|
41
|
+
const href =
|
|
42
|
+
options.href ??
|
|
43
|
+
"/bcp.css";
|
|
44
|
+
const maxBytes =
|
|
45
|
+
options.maxBytes ??
|
|
46
|
+
DEFAULT_INLINE_STYLESHEET_LIMIT;
|
|
47
|
+
|
|
48
|
+
assertValidMaxBytes(
|
|
49
|
+
maxBytes
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const stylesheetPattern =
|
|
53
|
+
createStylesheetPattern(
|
|
54
|
+
href
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (
|
|
58
|
+
!stylesheetPattern.test(
|
|
59
|
+
html
|
|
60
|
+
) ||
|
|
61
|
+
!canInlineStylesheet(
|
|
62
|
+
cssFile,
|
|
63
|
+
maxBytes
|
|
64
|
+
)
|
|
65
|
+
) {
|
|
66
|
+
return html;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const css =
|
|
70
|
+
fs.readFileSync(
|
|
71
|
+
cssFile,
|
|
72
|
+
"utf8"
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const safeCss =
|
|
76
|
+
css.replace(
|
|
77
|
+
/<\/style/gi,
|
|
78
|
+
"<\\/style"
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return html.replace(
|
|
82
|
+
stylesheetPattern,
|
|
83
|
+
`<style data-bcp-inline-css>${safeCss}</style>`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function resolveBcpStylesheetFile(
|
|
88
|
+
publicDirectory: string
|
|
89
|
+
): string {
|
|
90
|
+
return path.join(
|
|
91
|
+
publicDirectory,
|
|
92
|
+
"bcp.css"
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function assertValidMaxBytes(
|
|
97
|
+
maxBytes: number
|
|
98
|
+
): void {
|
|
99
|
+
if (
|
|
100
|
+
!Number.isFinite(
|
|
101
|
+
maxBytes
|
|
102
|
+
) ||
|
|
103
|
+
maxBytes < 0
|
|
104
|
+
) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
"BCP Framework: inline stylesheet maxBytes must be a non-negative finite number."
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function createStylesheetPattern(
|
|
112
|
+
href: string
|
|
113
|
+
): RegExp {
|
|
114
|
+
const escapedHref =
|
|
115
|
+
escapeRegExp(
|
|
116
|
+
href
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
return new RegExp(
|
|
120
|
+
`<link\\b(?=[^>]*\\brel=["']stylesheet["'])(?=[^>]*\\bhref=["']${escapedHref}(?:\\?[^"']*)?["'])[^>]*>`,
|
|
121
|
+
"i"
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function escapeRegExp(
|
|
126
|
+
value: string
|
|
127
|
+
): string {
|
|
128
|
+
return value.replace(
|
|
129
|
+
/[.*+?^${}()|[\]\\]/g,
|
|
130
|
+
"\\$&"
|
|
131
|
+
);
|
|
132
|
+
}
|