@chidchanun/bcp 0.1.5 → 0.1.6

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 CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  All notable framework changes are tracked here before release.
4
4
 
5
+ ## 0.1.6 - Critical CSS and release visibility
6
+
7
+ ### Performance
8
+
9
+ - Small generated `/bcp.css` stylesheets are inlined into SSR HTML when they are 8 KiB or smaller, removing the stylesheet request from the initial render-critical path.
10
+ - Critical CSS optimization is applied in development and standalone production.
11
+ - Gzip, Brotli and deflate HTML responses are decoded, transformed and re-encoded so response compression remains intact.
12
+ - Applications with CSP policies that do not allow inline styles automatically keep the external `/bcp.css` link instead of breaking page styling.
13
+ - Critical CSS transformation is skipped entirely when the stylesheet is missing or larger than the inline threshold.
14
+
15
+ ### Reliability and release tooling
16
+
17
+ - Added regression tests for critical CSS inlining, size fallback, closing-style escaping, compressed responses and CSP fallback.
18
+ - Added `release:visibility-check` so an npm release is not considered ready until the exact framework and generator versions are readable from the registry.
19
+ - `release:publish:yes` and `release:resume` now run the registry visibility check after npm accepts the publish.
20
+
5
21
  ## 0.1.5 - Application module boundaries
6
22
 
7
23
  ### Developer experience
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  BCP Framework is a React full-stack framework with file-based routing, SSR, client navigation, API routes, middleware, metadata, client islands, cache/revalidation, security defaults and standalone production builds.
4
4
 
5
- > Current development version: `0.1.5`. BCP is still pre-1.0 and validates each release candidate before the manual npm publish step.
5
+ > Current development version: `0.1.6`. BCP is still pre-1.0 and validates each release candidate before the manual npm publish step.
6
6
 
7
7
  ## Quick start
8
8
 
@@ -77,6 +77,10 @@ Database helpers generated by `create-bcp-app` include the server-only marker au
77
77
 
78
78
  See [Application Modules](docs/application-modules.md) for the complete boundary model and examples.
79
79
 
80
+ ## Tailwind and critical CSS
81
+
82
+ Generated Tailwind projects compile to `public/bcp.css`. BCP 0.1.6 inlines that stylesheet into SSR HTML when it is 8 KiB or smaller, removing the stylesheet request from the initial render-critical path. Larger stylesheets remain external so the browser can cache them normally. If the application's Content Security Policy does not allow inline styles, BCP automatically keeps the external stylesheet link.
83
+
80
84
  ## Commands
81
85
 
82
86
  ```bash
@@ -248,6 +252,8 @@ npm run rc:check
248
252
 
249
253
  `rc:check` validates tests/release metadata, checks npm package-name availability or ownership, performs `npm publish --dry-run`, and clean-installs both generated tarballs into temporary projects.
250
254
 
255
+ After a real publish, `npm run release:visibility-check` verifies that the exact framework and generator versions are readable from the npm registry before the release is treated as ready for installation.
256
+
251
257
  `package:prepare` stages the framework at `.package/bcp`. The default package name is `bcp`; set `BCP_PACKAGE_NAME` when preparing a scoped or alternate package name.
252
258
 
253
259
  No real npm publish command is run automatically by the repository.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chidchanun/bcp",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "BCP Framework - a React full-stack framework with file-based routing, SSR, APIs, middleware, islands, caching and standalone production builds.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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
+ }
@@ -5,6 +5,10 @@ import type {
5
5
  DevServerOptions,
6
6
  } from "./index.js";
7
7
 
8
+ import {
9
+ createCriticalCssProxy,
10
+ } from "./critical-css-proxy.js";
11
+
8
12
  import {
9
13
  loadProjectMiddleware,
10
14
  } from "./middleware-loader.js";
@@ -56,6 +60,12 @@ export function createMiddlewareDevServer(
56
60
  > | null =
57
61
  null;
58
62
 
63
+ let criticalCssGateway:
64
+ ReturnType<
65
+ typeof createCriticalCssProxy
66
+ > | null =
67
+ null;
68
+
59
69
  let started =
60
70
  false;
61
71
 
@@ -68,6 +78,8 @@ export function createMiddlewareDevServer(
68
78
  await findFreePort();
69
79
  const middlewarePort =
70
80
  await findFreePort();
81
+ const securityPort =
82
+ await findFreePort();
71
83
 
72
84
  internalServer =
73
85
  createStaticDevServer({
@@ -111,8 +123,10 @@ export function createMiddlewareDevServer(
111
123
 
112
124
  securityGateway =
113
125
  createSecurityProxy({
114
- port,
115
- hostname,
126
+ port:
127
+ securityPort,
128
+ hostname:
129
+ "127.0.0.1",
116
130
  upstreamPort:
117
131
  middlewarePort,
118
132
  upstreamHostname:
@@ -133,12 +147,48 @@ export function createMiddlewareDevServer(
133
147
  throw error;
134
148
  }
135
149
 
150
+ criticalCssGateway =
151
+ createCriticalCssProxy({
152
+ port,
153
+ hostname,
154
+ upstreamPort:
155
+ securityPort,
156
+ upstreamHostname:
157
+ "127.0.0.1",
158
+ cssFile:
159
+ path.join(
160
+ rootDirectory,
161
+ "public",
162
+ "bcp.css"
163
+ ),
164
+ });
165
+
166
+ try {
167
+ await criticalCssGateway.start();
168
+ } catch (error) {
169
+ await securityGateway.stop();
170
+ await middlewareGateway.stop();
171
+ await internalServer.stop();
172
+ internalServer =
173
+ null;
174
+ middlewareGateway =
175
+ null;
176
+ securityGateway =
177
+ null;
178
+ criticalCssGateway =
179
+ null;
180
+ throw error;
181
+ }
182
+
136
183
  started =
137
184
  true;
138
185
 
139
186
  console.log(
140
187
  `[BCP Security] Dev gateway: http://${hostname}:${port}`
141
188
  );
189
+ console.log(
190
+ `[BCP CSS] Inline /bcp.css when <= 8 KiB and allowed by CSP.`
191
+ );
142
192
  console.log(
143
193
  `[BCP Middleware] File: ${path.join(rootDirectory, "middleware.ts")} (optional; .tsx/.js/.jsx also supported)`
144
194
  );
@@ -146,6 +196,12 @@ export function createMiddlewareDevServer(
146
196
  }
147
197
 
148
198
  async function stop(): Promise<void> {
199
+ if (criticalCssGateway) {
200
+ await criticalCssGateway.stop();
201
+ criticalCssGateway =
202
+ null;
203
+ }
204
+
149
205
  if (securityGateway) {
150
206
  await securityGateway.stop();
151
207
  securityGateway =
@@ -257,4 +313,4 @@ async function findFreePort(): Promise<number> {
257
313
  );
258
314
  }
259
315
  }
260
- }
316
+ }
@@ -0,0 +1,205 @@
1
+ import * as net from "node:net";
2
+ import path from "node:path";
3
+
4
+ import {
5
+ createCriticalCssProxy,
6
+ } from "./critical-css-proxy.js";
7
+
8
+ import {
9
+ createStandaloneProductionServer as createBaseStandaloneProductionServer,
10
+ type StandaloneApiRoute,
11
+ type StandalonePageRoute,
12
+ type StandaloneProductionServerOptions as BaseStandaloneProductionServerOptions,
13
+ } from "./standalone-production-runtime-v5.js";
14
+
15
+ export type {
16
+ StandaloneApiRoute,
17
+ StandalonePageRoute,
18
+ };
19
+
20
+ export interface StandaloneProductionServerOptions
21
+ extends BaseStandaloneProductionServerOptions {}
22
+
23
+ export function createStandaloneProductionServer(
24
+ options: StandaloneProductionServerOptions
25
+ ) {
26
+ let baseServer:
27
+ ReturnType<
28
+ typeof createBaseStandaloneProductionServer
29
+ > | null =
30
+ null;
31
+
32
+ let criticalCssProxy:
33
+ ReturnType<
34
+ typeof createCriticalCssProxy
35
+ > | null =
36
+ null;
37
+
38
+ let started =
39
+ false;
40
+
41
+ async function start(): Promise<void> {
42
+ if (started) {
43
+ return;
44
+ }
45
+
46
+ const internalPort =
47
+ await findFreePort();
48
+
49
+ baseServer =
50
+ createBaseStandaloneProductionServer({
51
+ ...options,
52
+ port:
53
+ internalPort,
54
+ hostname:
55
+ "127.0.0.1",
56
+ });
57
+
58
+ await baseServer.start();
59
+
60
+ criticalCssProxy =
61
+ createCriticalCssProxy({
62
+ port:
63
+ options.port,
64
+ hostname:
65
+ options.hostname,
66
+ upstreamPort:
67
+ internalPort,
68
+ upstreamHostname:
69
+ "127.0.0.1",
70
+ cssFile:
71
+ path.join(
72
+ options.buildDirectory,
73
+ "public",
74
+ "bcp.css"
75
+ ),
76
+ });
77
+
78
+ try {
79
+ await criticalCssProxy.start();
80
+ } catch (error) {
81
+ await baseServer.stop();
82
+ baseServer =
83
+ null;
84
+ criticalCssProxy =
85
+ null;
86
+ throw error;
87
+ }
88
+
89
+ started =
90
+ true;
91
+
92
+ console.log(
93
+ `[BCP CSS] Inline /bcp.css when <= 8 KiB.`
94
+ );
95
+ console.log("");
96
+ }
97
+
98
+ async function stop(): Promise<void> {
99
+ if (criticalCssProxy) {
100
+ await criticalCssProxy.stop();
101
+ criticalCssProxy =
102
+ null;
103
+ }
104
+
105
+ if (baseServer) {
106
+ await baseServer.stop();
107
+ baseServer =
108
+ null;
109
+ }
110
+
111
+ started =
112
+ false;
113
+ }
114
+
115
+ return {
116
+ start,
117
+ stop,
118
+ };
119
+ }
120
+
121
+ async function findFreePort(): Promise<number> {
122
+ const server =
123
+ net.createServer();
124
+
125
+ try {
126
+ await new Promise<void>(
127
+ (
128
+ resolve,
129
+ reject
130
+ ) => {
131
+ const onError =
132
+ (error: Error) => {
133
+ server.off(
134
+ "listening",
135
+ onListening
136
+ );
137
+ reject(
138
+ error
139
+ );
140
+ };
141
+
142
+ const onListening =
143
+ () => {
144
+ server.off(
145
+ "error",
146
+ onError
147
+ );
148
+ resolve();
149
+ };
150
+
151
+ server.once(
152
+ "error",
153
+ onError
154
+ );
155
+ server.once(
156
+ "listening",
157
+ onListening
158
+ );
159
+ server.listen(
160
+ 0,
161
+ "127.0.0.1"
162
+ );
163
+ }
164
+ );
165
+
166
+ const address =
167
+ server.address();
168
+
169
+ if (
170
+ !address ||
171
+ typeof address ===
172
+ "string"
173
+ ) {
174
+ throw new Error(
175
+ "BCP Framework: could not allocate critical CSS production port."
176
+ );
177
+ }
178
+
179
+ return address.port;
180
+ } finally {
181
+ if (
182
+ server.listening
183
+ ) {
184
+ await new Promise<void>(
185
+ (
186
+ resolve,
187
+ reject
188
+ ) => {
189
+ server.close(
190
+ (error) => {
191
+ if (error) {
192
+ reject(
193
+ error
194
+ );
195
+ return;
196
+ }
197
+
198
+ resolve();
199
+ }
200
+ );
201
+ }
202
+ );
203
+ }
204
+ }
205
+ }
@@ -3,4 +3,4 @@ export {
3
3
  type StandaloneApiRoute,
4
4
  type StandalonePageRoute,
5
5
  type StandaloneProductionServerOptions,
6
- } from "./standalone-production-runtime-v5.js";
6
+ } from "./standalone-production-runtime-v6.js";