@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 @@
1
+ export * from "./advanced-router.js";
@@ -0,0 +1,249 @@
1
+ import type * as http from "node:http";
2
+ import {
3
+ brotliCompressSync,
4
+ constants,
5
+ gzipSync,
6
+ } from "node:zlib";
7
+
8
+ export type ContentEncoding =
9
+ | "br"
10
+ | "gzip"
11
+ | null;
12
+
13
+ const MIN_COMPRESSION_BYTES =
14
+ 1024;
15
+
16
+ export function selectContentEncoding(
17
+ req: http.IncomingMessage,
18
+ contentType: string,
19
+ contentLength: number,
20
+ available: {
21
+ br?: boolean;
22
+ gzip?: boolean;
23
+ } = {
24
+ br: true,
25
+ gzip: true,
26
+ }
27
+ ): ContentEncoding {
28
+ if (
29
+ process.env
30
+ .BCP_COMPRESSION ===
31
+ "0"
32
+ ) {
33
+ return null;
34
+ }
35
+
36
+ if (
37
+ contentLength <
38
+ MIN_COMPRESSION_BYTES ||
39
+ !isCompressibleContentType(
40
+ contentType
41
+ )
42
+ ) {
43
+ return null;
44
+ }
45
+
46
+ const header =
47
+ req.headers[
48
+ "accept-encoding"
49
+ ];
50
+
51
+ const value =
52
+ Array.isArray(header)
53
+ ? header.join(",")
54
+ : header ?? "";
55
+
56
+ const brQuality =
57
+ available.br === false
58
+ ? 0
59
+ : getEncodingQuality(
60
+ value,
61
+ "br"
62
+ );
63
+
64
+ const gzipQuality =
65
+ available.gzip === false
66
+ ? 0
67
+ : getEncodingQuality(
68
+ value,
69
+ "gzip"
70
+ );
71
+
72
+ if (
73
+ brQuality > 0 &&
74
+ brQuality >= gzipQuality
75
+ ) {
76
+ return "br";
77
+ }
78
+
79
+ if (
80
+ gzipQuality > 0
81
+ ) {
82
+ return "gzip";
83
+ }
84
+
85
+ return null;
86
+ }
87
+
88
+ export function compressDynamicContent(
89
+ content: Buffer,
90
+ encoding: ContentEncoding
91
+ ): Buffer {
92
+ if (
93
+ encoding === "br"
94
+ ) {
95
+ return brotliCompressSync(
96
+ content,
97
+ {
98
+ params: {
99
+ [constants.BROTLI_PARAM_QUALITY]:
100
+ 4,
101
+ },
102
+ }
103
+ );
104
+ }
105
+
106
+ if (
107
+ encoding === "gzip"
108
+ ) {
109
+ return gzipSync(
110
+ content,
111
+ {
112
+ level: 6,
113
+ }
114
+ );
115
+ }
116
+
117
+ return content;
118
+ }
119
+
120
+ export function isCompressibleContentType(
121
+ contentType: string
122
+ ): boolean {
123
+ const normalized =
124
+ contentType
125
+ .split(";", 1)[0]
126
+ .trim()
127
+ .toLowerCase();
128
+
129
+ return (
130
+ normalized.startsWith(
131
+ "text/"
132
+ ) ||
133
+ normalized ===
134
+ "application/javascript" ||
135
+ normalized ===
136
+ "application/json" ||
137
+ normalized ===
138
+ "application/xml" ||
139
+ normalized ===
140
+ "application/manifest+json" ||
141
+ normalized ===
142
+ "image/svg+xml"
143
+ );
144
+ }
145
+
146
+ export function appendVary(
147
+ existing: string | undefined,
148
+ value: string
149
+ ): string {
150
+ if (!existing) {
151
+ return value;
152
+ }
153
+
154
+ const values =
155
+ existing
156
+ .split(",")
157
+ .map(
158
+ (item) =>
159
+ item.trim()
160
+ );
161
+
162
+ if (
163
+ values.some(
164
+ (item) =>
165
+ item.toLowerCase() ===
166
+ value.toLowerCase()
167
+ )
168
+ ) {
169
+ return existing;
170
+ }
171
+
172
+ return `${existing}, ${value}`;
173
+ }
174
+
175
+ function getEncodingQuality(
176
+ header: string,
177
+ encoding: string
178
+ ): number {
179
+ let wildcardQuality:
180
+ number | null =
181
+ null;
182
+
183
+ for (
184
+ const item
185
+ of header.split(",")
186
+ ) {
187
+ const [
188
+ tokenPart,
189
+ ...parameters
190
+ ] =
191
+ item.split(";");
192
+
193
+ const token =
194
+ tokenPart
195
+ .trim()
196
+ .toLowerCase();
197
+
198
+ if (!token) {
199
+ continue;
200
+ }
201
+
202
+ let quality = 1;
203
+
204
+ for (
205
+ const parameter
206
+ of parameters
207
+ ) {
208
+ const match =
209
+ /^\s*q\s*=\s*(\d(?:\.\d+)?)\s*$/i.exec(
210
+ parameter
211
+ );
212
+
213
+ if (match) {
214
+ const parsed =
215
+ Number(
216
+ match[1]
217
+ );
218
+
219
+ quality =
220
+ Number.isFinite(
221
+ parsed
222
+ )
223
+ ? Math.max(
224
+ 0,
225
+ Math.min(
226
+ 1,
227
+ parsed
228
+ )
229
+ )
230
+ : 0;
231
+ }
232
+ }
233
+
234
+ if (
235
+ token === encoding
236
+ ) {
237
+ return quality;
238
+ }
239
+
240
+ if (
241
+ token === "*"
242
+ ) {
243
+ wildcardQuality =
244
+ quality;
245
+ }
246
+ }
247
+
248
+ return wildcardQuality ?? 0;
249
+ }
@@ -0,0 +1,154 @@
1
+ import {
2
+ createMetadataHeadEntries,
3
+ type ResolvedMetadata,
4
+ } from "../../client/src/metadata.js";
5
+
6
+ export function applyDevDocumentMetadata(
7
+ html: string,
8
+ metadata: ResolvedMetadata
9
+ ): string {
10
+ const title =
11
+ `<title>${escapeHtmlText(
12
+ metadata.title
13
+ )}</title>`;
14
+
15
+ let result =
16
+ /<title\b[^>]*>[\s\S]*?<\/title>/i.test(
17
+ html
18
+ )
19
+ ? html.replace(
20
+ /<title\b[^>]*>[\s\S]*?<\/title>/i,
21
+ title
22
+ )
23
+ : html.replace(
24
+ /<\/head>/i,
25
+ ` ${title}\n </head>`
26
+ );
27
+
28
+ /*
29
+ * Keep this transformation idempotent. This lets the outer dev server
30
+ * remain correct if the inner renderer starts emitting metadata itself
31
+ * in a future implementation.
32
+ */
33
+ result =
34
+ result.replace(
35
+ /\s*<(?:meta|link)\b[^>]*\bdata-bcp-metadata=(?:"[^"]*"|'[^']*')[^>]*\/?\s*>/gi,
36
+ ""
37
+ );
38
+
39
+ const entries =
40
+ createMetadataHeadEntries(
41
+ metadata
42
+ );
43
+
44
+ if (
45
+ entries.length > 0
46
+ ) {
47
+ const head =
48
+ entries
49
+ .map(
50
+ (entry) => {
51
+ const attributes = [
52
+ [
53
+ "data-bcp-metadata",
54
+ entry.key,
55
+ ] as const,
56
+ ...Object.entries(
57
+ entry.attributes
58
+ ),
59
+ ]
60
+ .map(
61
+ ([name, value]) =>
62
+ `${name}="${escapeHtmlAttribute(
63
+ value
64
+ )}"`
65
+ )
66
+ .join(" ");
67
+
68
+ return (
69
+ entry.tag === "meta"
70
+ ? `<meta ${attributes} />`
71
+ : `<link ${attributes} />`
72
+ );
73
+ }
74
+ )
75
+ .join("\n ");
76
+
77
+ result =
78
+ result.replace(
79
+ /<\/head>/i,
80
+ ` ${head}\n </head>`
81
+ );
82
+ }
83
+
84
+ return attachFrameworkMetadata(
85
+ result,
86
+ metadata
87
+ );
88
+ }
89
+
90
+ function attachFrameworkMetadata(
91
+ html: string,
92
+ metadata: ResolvedMetadata
93
+ ): string {
94
+ return html.replace(
95
+ /(<script\s+id="__BCP_DATA__"\s+type="application\/json"\s*>)([\s\S]*?)(<\/script>)/i,
96
+ (
97
+ match,
98
+ openTag: string,
99
+ serialized: string,
100
+ closeTag: string
101
+ ) => {
102
+ try {
103
+ const data =
104
+ JSON.parse(
105
+ serialized.trim()
106
+ ) as Record<
107
+ string,
108
+ unknown
109
+ >;
110
+
111
+ data.metadata =
112
+ metadata;
113
+
114
+ return (
115
+ `${openTag}${serializeJsonForHtml(
116
+ data
117
+ )}${closeTag}`
118
+ );
119
+ } catch {
120
+ return match;
121
+ }
122
+ }
123
+ );
124
+ }
125
+
126
+ function serializeJsonForHtml(
127
+ value: unknown
128
+ ): string {
129
+ return JSON.stringify(
130
+ value
131
+ )
132
+ .replace(/</g, "\\u003c")
133
+ .replace(/\u2028/g, "\\u2028")
134
+ .replace(/\u2029/g, "\\u2029");
135
+ }
136
+
137
+ function escapeHtmlText(
138
+ value: string
139
+ ): string {
140
+ return value
141
+ .replace(/&/g, "&amp;")
142
+ .replace(/</g, "&lt;")
143
+ .replace(/>/g, "&gt;");
144
+ }
145
+
146
+ function escapeHtmlAttribute(
147
+ value: string
148
+ ): string {
149
+ return escapeHtmlText(
150
+ value
151
+ )
152
+ .replace(/"/g, "&quot;")
153
+ .replace(/'/g, "&#39;");
154
+ }
@@ -0,0 +1,225 @@
1
+ import type * as http from "node:http";
2
+
3
+ export const DEV_EVENTS_PATH =
4
+ "/_bcp/dev-events";
5
+
6
+ const ENV_RESTART_COOKIE =
7
+ "__bcp_env_restart";
8
+
9
+ export class DevHmrServer {
10
+ private readonly clients =
11
+ new Set<
12
+ http.ServerResponse
13
+ >();
14
+
15
+ connect(
16
+ req: http.IncomingMessage,
17
+ res: http.ServerResponse
18
+ ) {
19
+ const restartToken =
20
+ process.env
21
+ .BCP_DEV_ENV_RESTART ??
22
+ "0";
23
+
24
+ const shouldReload =
25
+ restartToken !== "0" &&
26
+ readCookie(
27
+ req,
28
+ ENV_RESTART_COOKIE
29
+ ) !== restartToken;
30
+
31
+ const headers:
32
+ Record<string, string> = {
33
+ "Content-Type":
34
+ "text/event-stream; charset=utf-8",
35
+ "Cache-Control":
36
+ "no-cache, no-store",
37
+ Connection:
38
+ "keep-alive",
39
+ };
40
+
41
+ if (shouldReload) {
42
+ headers[
43
+ "Set-Cookie"
44
+ ] =
45
+ `${ENV_RESTART_COOKIE}=${encodeURIComponent(restartToken)}; Path=/; SameSite=Lax`;
46
+ }
47
+
48
+ res.writeHead(
49
+ 200,
50
+ headers
51
+ );
52
+
53
+ res.write(
54
+ ": bcp-hmr-connected\n\n"
55
+ );
56
+
57
+ this.clients.add(
58
+ res
59
+ );
60
+
61
+ if (shouldReload) {
62
+ this.emitTo(
63
+ res,
64
+ "reload",
65
+ {
66
+ version:
67
+ Date.now(),
68
+ reason:
69
+ "environment",
70
+ }
71
+ );
72
+ }
73
+
74
+ const cleanup =
75
+ () => {
76
+ this.clients.delete(
77
+ res
78
+ );
79
+ };
80
+
81
+ req.once(
82
+ "close",
83
+ cleanup
84
+ );
85
+
86
+ res.once(
87
+ "close",
88
+ cleanup
89
+ );
90
+ }
91
+
92
+ refresh(
93
+ version: number,
94
+ routes: string[]
95
+ ) {
96
+ this.emit(
97
+ "refresh",
98
+ {
99
+ version,
100
+ routes,
101
+ }
102
+ );
103
+ }
104
+
105
+ reload(
106
+ version: number
107
+ ) {
108
+ this.emit(
109
+ "reload",
110
+ {
111
+ version,
112
+ }
113
+ );
114
+ }
115
+
116
+ close() {
117
+ for (
118
+ const client
119
+ of this.clients
120
+ ) {
121
+ client.end();
122
+ }
123
+
124
+ this.clients.clear();
125
+ }
126
+
127
+ private emit(
128
+ event: string,
129
+ data: unknown
130
+ ) {
131
+ for (
132
+ const client
133
+ of this.clients
134
+ ) {
135
+ if (
136
+ client.destroyed ||
137
+ client.writableEnded
138
+ ) {
139
+ this.clients.delete(
140
+ client
141
+ );
142
+
143
+ continue;
144
+ }
145
+
146
+ this.emitTo(
147
+ client,
148
+ event,
149
+ data
150
+ );
151
+ }
152
+ }
153
+
154
+ private emitTo(
155
+ client: http.ServerResponse,
156
+ event: string,
157
+ data: unknown
158
+ ): void {
159
+ const payload =
160
+ JSON.stringify(
161
+ data
162
+ );
163
+
164
+ client.write(
165
+ `event: ${event}\n`
166
+ );
167
+
168
+ client.write(
169
+ `data: ${payload}\n\n`
170
+ );
171
+ }
172
+ }
173
+
174
+ function readCookie(
175
+ req: http.IncomingMessage,
176
+ name: string
177
+ ): string | null {
178
+ const header =
179
+ req.headers.cookie;
180
+
181
+ if (!header) {
182
+ return null;
183
+ }
184
+
185
+ for (
186
+ const item
187
+ of header.split(";")
188
+ ) {
189
+ const separator =
190
+ item.indexOf("=");
191
+
192
+ if (separator < 0) {
193
+ continue;
194
+ }
195
+
196
+ const key =
197
+ item
198
+ .slice(
199
+ 0,
200
+ separator
201
+ )
202
+ .trim();
203
+
204
+ if (key !== name) {
205
+ continue;
206
+ }
207
+
208
+ const value =
209
+ item
210
+ .slice(
211
+ separator + 1
212
+ )
213
+ .trim();
214
+
215
+ try {
216
+ return decodeURIComponent(
217
+ value
218
+ );
219
+ } catch {
220
+ return value;
221
+ }
222
+ }
223
+
224
+ return null;
225
+ }